LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_petri.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_H
6#define LINE_SOLVERS_FLUID_PETRI_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Fluid analysis of a stochastic Petri net: one simultaneous algebraic solve per
12 * active set.
13 *
14 * Port of `matlab/src/solvers/FLD/solver_fluid_petri.m`, together with
15 * `fluid_petri_conservation.m`, `fluid_petri_constraints.m`,
16 * `fluid_petri_immediate.m` and `fluid_petri_applicable.m`. Cross-checked
17 * against `jar/src/main/java/jline/solvers/fluid/petri/PetriSolver.java` and the
18 * python `solver_fld/methods/petri.py`.
19 *
20 * A net's fluid limit is NOT the queueing drift with places in place of
21 * stations. Its immediate transitions have no rate at all: their limit is a
22 * FLOW, an algebraic unknown pinned by the constraint that the input place they
23 * bind holds no mass. The marking, the diffusion covariance, those flows, the
24 * multi-server phase latches and the capacity gates therefore solve
25 * SIMULTANEOUSLY, as one algebraic system per active set, rather than by
26 * integrating an ODE to its fixed point.
27 *
28 * THE ACTIVE SET IS WHAT ITERATES. A pass solves the system for a fixed choice
29 * of which immediate modes fire, which coordinate each pins, and which
30 * capacities bind; the answer says whether that choice was right (a negative
31 * flow, a negative marking, a violated or a released capacity), and the next
32 * pass makes ONE move. The moves are ordered so that a failure of each
33 * invalidates the next.
34 */
35
36#include <algorithm>
37#include <cctype>
38#include <cmath>
39#include <cstddef>
40#include <cstdio>
41#include <limits>
42#include <map>
43#include <set>
44#include <string>
45#include <vector>
46
51#include "line/util/error.h"
52#include "line/util/linalg.h"
53#include "line/util/matrix.h"
54#include "line/util/ode.h"
55#include "line/util/svd.h"
56#include "rodas.hpp"
57
58namespace line {
59namespace fluid {
60namespace petri {
61
62// ============================ conservation ===============================
63
64/** The conserved quantities of a net, as equations. */
67 std::vector<double> N;
68 double leak = 0.0;
69 std::vector<std::string> label;
70};
71
72namespace cons_detail {
73
74/**
75 * A rational basis of the null space, as MATLAB's `null(A,'r')` returns.
76 *
77 * D is INTEGRAL -- arc multiplicities and unit phase moves -- so the reduced row
78 * echelon form gives exact rational rows, which keeps each conservation row
79 * readable as a statement about named places instead of an arbitrary orthogonal
80 * mixture. An SVD basis would span the same space and say nothing.
81 */
82inline Matrix<double> null_rational(const Matrix<double>& A) {
83 const std::size_t rows = A.rows(), cols = A.cols();
84 if (rows == 0 || cols == 0) return Matrix<double>(cols, 0, 0.0);
85 Matrix<double> R = A;
86 std::vector<std::size_t> piv;
87 std::size_t r = 0;
88 const double tol = 1e-12;
89 for (std::size_t c = 0; c < cols && r < rows; ++c) {
90 std::size_t k = r;
91 double best = std::fabs(R(r, c));
92 for (std::size_t i = r + 1; i < rows; ++i)
93 if (std::fabs(R(i, c)) > best) {
94 best = std::fabs(R(i, c));
95 k = i;
96 }
97 if (best <= tol) {
98 for (std::size_t i = r; i < rows; ++i) R(i, c) = 0.0;
99 continue;
100 }
101 if (k != r)
102 for (std::size_t j = 0; j < cols; ++j) std::swap(R(r, j), R(k, j));
103 const double p = R(r, c);
104 for (std::size_t j = 0; j < cols; ++j) R(r, j) /= p;
105 for (std::size_t i = 0; i < rows; ++i) {
106 if (i == r) continue;
107 const double f = R(i, c);
108 if (f == 0.0) continue;
109 for (std::size_t j = 0; j < cols; ++j) R(i, j) -= f * R(r, j);
110 }
111 piv.push_back(c);
112 ++r;
113 }
114 std::vector<std::size_t> free;
115 for (std::size_t c = 0; c < cols; ++c)
116 if (std::find(piv.begin(), piv.end(), c) == piv.end()) free.push_back(c);
117 Matrix<double> Z(cols, free.size(), 0.0);
118 for (std::size_t i = 0; i < free.size(); ++i) {
119 Z(free[i], i) = 1.0;
120 for (std::size_t rr = 0; rr < piv.size(); ++rr) Z(piv[rr], i) = -R(rr, free[i]);
121 }
122 return Z;
123}
124
125/** %g formatting, so an integral weight prints without a decimal tail. */
126inline std::string trim(double v) {
127 if (v == std::rint(v) && std::fabs(v) < 1e15)
128 return std::to_string(static_cast<long long>(v));
129 char buf[32];
130 std::snprintf(buf, sizeof(buf), "%g", v);
131 return std::string(buf);
132}
133
134/** One conservation row, written out over the coordinates it touches. */
135inline std::string row_label(const PetriTerms& t, const Matrix<double>& C, std::size_t r) {
136 std::string out;
137 for (std::size_t s = 0; s < C.cols(); ++s) {
138 const double w = C(r, s);
139 if (w == 0.0) continue;
140 std::string nm;
141 if (s < t.nm) {
142 nm = t.names_node[t.coord_node[s]] + "(class " +
143 std::to_string(t.coord_class[s] + 1) + ")";
144 } else {
145 nm = "phase";
146 for (std::size_t q = 0; q < t.nf; ++q)
147 if (t.fluid_slot[q] == s) nm = "level of " + t.fluid_name[q];
148 for (std::size_t j = 0; j < t.modes.size(); ++j)
149 for (std::size_t q = 0; q < t.modes[j].zblk.size(); ++q)
150 if (t.modes[j].zblk[q] == s)
151 nm = t.modes[j].label + " phase " + std::to_string(q + 1);
152 }
153 if (!out.empty()) out += " + ";
154 out += (w == 1.0) ? nm : (trim(w) + "*" + nm);
155 }
156 return out;
157}
158
159} // namespace cons_detail
160
161/**
162 * The conserved quantities, as equations: `u'D = 0` implies `u'x` is constant.
163 *
164 * On the marking coordinates those u are the net's P-invariants; on a mode's
165 * phase block the all-ones vector is one of them, which is the statement that
166 * the phase coordinates are a distribution. Both come out of the SAME null
167 * space, so the phase normalisation needs no separate row.
168 *
169 * AN OPEN NET LOSES THE ROWS ITS ARRIVALS BREAK, automatically: the arrival
170 * columns are part of D, so a u an arrival moves is not in the null space.
171 */
174 if (t.D.rows() == 0 || t.D.cols() == 0 || t.nev == 0) {
175 cons.C = Matrix<double>(0, t.nstate, 0.0);
176 return cons;
177 }
178 Matrix<double> Dt(t.D.cols(), t.D.rows(), 0.0);
179 for (std::size_t i = 0; i < t.D.rows(); ++i)
180 for (std::size_t j = 0; j < t.D.cols(); ++j) Dt(j, i) = t.D(i, j);
181 const Matrix<double> Z = cons_detail::null_rational(Dt);
182 Matrix<double> C(Z.cols(), Z.rows(), 0.0);
183 for (std::size_t i = 0; i < Z.rows(); ++i)
184 for (std::size_t j = 0; j < Z.cols(); ++j) C(j, i) = Z(i, j);
185 // A rational basis has exact zeros; anything below this is a rounding
186 // artefact of the elimination, not a coefficient.
187 for (std::size_t i = 0; i < C.rows(); ++i)
188 for (std::size_t j = 0; j < C.cols(); ++j)
189 if (std::fabs(C(i, j)) < 1e-12) C(i, j) = 0.0;
190 // Scale each row by its smallest nonzero magnitude, so a row reads as a
191 // statement about named places with small integer weights.
192 std::vector<std::size_t> keep;
193 for (std::size_t i = 0; i < C.rows(); ++i) {
194 double smallest = std::numeric_limits<double>::infinity();
195 for (std::size_t j = 0; j < C.cols(); ++j)
196 if (C(i, j) != 0.0) smallest = std::min(smallest, std::fabs(C(i, j)));
197 if (!std::isfinite(smallest)) continue;
198 for (std::size_t j = 0; j < C.cols(); ++j) C(i, j) /= smallest;
199 keep.push_back(i);
200 }
201 cons.C = Matrix<double>(keep.size(), t.nstate, 0.0);
202 for (std::size_t r = 0; r < keep.size(); ++r)
203 for (std::size_t j = 0; j < t.nstate; ++j) cons.C(r, j) = C(keep[r], j);
204 cons.N.assign(cons.C.rows(), 0.0);
205 for (std::size_t r = 0; r < cons.C.rows(); ++r) {
206 double v = 0.0;
207 for (std::size_t j = 0; j < t.nstate; ++j) v += cons.C(r, j) * t.x0[j];
208 cons.N[r] = v;
209 }
210 for (std::size_t r = 0; r < cons.C.rows(); ++r)
211 for (std::size_t c = 0; c < t.nev; ++c) {
212 double v = 0.0;
213 for (std::size_t j = 0; j < t.nstate; ++j) v += cons.C(r, j) * t.D(j, c);
214 cons.leak = std::max(cons.leak, std::fabs(v));
215 }
216 for (std::size_t r = 0; r < cons.C.rows(); ++r)
217 cons.label.push_back(cons_detail::row_label(t, cons.C, r));
218 return cons;
219}
220
221// ============================ capacities =================================
222
223/**
224 * Every finite place capacity as a linear row `A x <= b`, and the two bounds of every fluid place.
225 *
226 * `cover[c]` names the coordinates row c caps, i.e. the deposits (or, for a LOWER row, the withdrawals) its
227 * multiplier throttles. `lower[c]` marks the empty-level row of a fluid place, whose multiplier gates the
228 * withdrawals, and `fluid[c]` any bound of a fluid place.
229 */
232 std::vector<double> b;
233 std::vector<std::string> label;
234 std::vector<std::vector<bool>> cover;
235 std::vector<bool> lower;
236 std::vector<bool> fluid;
237};
238
239/**
240 * THE GATE IS A LOSS ON THE DEPOSIT: LINE loses the tokens a firing would push
241 * past a place's capacity, so the fluid analogue scales the DEPOSIT leg of every
242 * event adding mass to the capped place and leaves the removal leg alone. The
243 * rows here name which coordinates are capped; the scaling lives in the tangent
244 * clamp.
245 *
246 * A FLUID PLACE HAS TWO BOUNDS, and the lower one is new. Its capacity B is a row of the same kind, throttling
247 * the inflow legs (flows into it and positive impulses) while it binds. Its EMPTY level is a row the token places
248 * never needed: a token drain slows down with the marking through min(), so a place empties asymptotically, but
249 * a fluid drain runs at its full rate down to zero. The row -f <= 0 throttles the OUTFLOW legs (flows out of it
250 * and negative impulses) while it binds: an empty place supplies no more than its inflow, and a firing is never
251 * blocked, only its withdrawal truncated.
252 */
253template <class T>
255 std::vector<std::vector<double>> rows;
256 std::vector<double> bs;
257 std::vector<std::string> labels;
258 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
259 const std::size_t ind = t.places[pi];
260 const std::size_t ist = sn.nodes[ind - 1].station;
261 std::vector<std::size_t> slots;
262 for (std::size_t k = 0; k < t.K; ++k)
263 if (t.pidx[ind - 1][k] >= 0)
264 slots.push_back(static_cast<std::size_t>(t.pidx[ind - 1][k]));
265 if (slots.empty()) continue;
266 if (ist >= 1 && ist <= sn.stations.size()) {
267 const double cap = sn.stations[ist - 1].cap;
268 if (std::isfinite(cap)) {
269 std::vector<double> row(t.nstate, 0.0);
270 for (std::size_t s : slots) row[s] = 1.0;
271 rows.push_back(row);
272 bs.push_back(cap);
273 labels.push_back("capacity " + cons_detail::trim(cap) + " of place " +
274 t.names_node[ind - 1]);
275 }
276 for (std::size_t k = 0; k < t.K; ++k) {
277 if (t.pidx[ind - 1][k] < 0) continue;
278 if (k >= sn.stations[ist - 1].classcap.size()) continue;
279 const double ccap = sn.stations[ist - 1].classcap[k];
280 if (!std::isfinite(ccap)) continue;
281 std::vector<double> row(t.nstate, 0.0);
282 row[static_cast<std::size_t>(t.pidx[ind - 1][k])] = 1.0;
283 rows.push_back(row);
284 bs.push_back(ccap);
285 labels.push_back("class-" + std::to_string(k + 1) + " capacity " +
286 cons_detail::trim(ccap) + " of place " + t.names_node[ind - 1]);
287 }
288 }
289 }
290 // TWO ROWS THAT SAY THE SAME THING ARE A SINGULAR NEWTON SYSTEM, not a
291 // redundancy the least squares absorbs, so an exact duplicate is pruned and
292 // the tighter bound survives.
293 std::vector<bool> keep(rows.size(), true);
294 for (std::size_t c = 0; c < rows.size(); ++c) {
295 if (!keep[c]) continue;
296 for (std::size_t d = c + 1; d < rows.size(); ++d) {
297 if (!keep[d] || rows[c] != rows[d]) continue;
298 if (bs[d] < bs[c]) {
299 keep[c] = false;
300 break;
301 }
302 keep[d] = false;
303 }
304 }
305 std::vector<std::size_t> idx;
306 for (std::size_t i = 0; i < keep.size(); ++i)
307 if (keep[i]) idx.push_back(i);
308 std::vector<bool> lowers(idx.size(), false), fluids(idx.size(), false);
309 for (std::size_t q = 0; q < t.nf; ++q) {
310 const std::size_t slot = t.fluid_slot[q];
311 if (std::isfinite(t.fluid_cap[q])) {
312 std::vector<double> row(t.nstate, 0.0);
313 row[slot] = 1.0;
314 rows.push_back(row);
315 bs.push_back(t.fluid_cap[q]);
316 char buf[64];
317 std::snprintf(buf, sizeof(buf), "%g", t.fluid_cap[q]);
318 labels.push_back("fluid capacity " + std::string(buf) + " of place " + t.fluid_name[q]);
319 idx.push_back(rows.size() - 1);
320 lowers.push_back(false);
321 fluids.push_back(true);
322 }
323 std::vector<double> row(t.nstate, 0.0);
324 row[slot] = -1.0;
325 rows.push_back(row);
326 bs.push_back(0.0);
327 labels.push_back("empty level of fluid place " + t.fluid_name[q]);
328 idx.push_back(rows.size() - 1);
329 lowers.push_back(true);
330 fluids.push_back(true);
331 }
333 con.A = Matrix<double>(idx.size(), t.nstate, 0.0);
334 con.b.assign(idx.size(), 0.0);
335 con.cover.assign(idx.size(), std::vector<bool>(t.nstate, false));
336 for (std::size_t r = 0; r < idx.size(); ++r) {
337 for (std::size_t s = 0; s < t.nstate; ++s) {
338 con.A(r, s) = rows[idx[r]][s];
339 con.cover[r][s] = rows[idx[r]][s] != 0.0;
340 }
341 con.b[r] = bs[idx[r]];
342 con.label.push_back(labels[idx[r]]);
343 }
344 con.lower = lowers;
345 con.fluid = fluids;
346 return con;
347}
348
349// ============================ the immediates =============================
350
351/** The active set of the immediate modes and the equations pinning their flows. */
353 enum Kind { PIN = 0, RATIO = 1, ZERO = 2 };
354 struct Row {
356 std::size_t a = 0, b = 0;
357 double wa = 0.0, wb = 0.0;
358 };
359 std::size_t n = 0;
360 std::vector<bool> active;
361 std::vector<std::ptrdiff_t> bind;
362 std::vector<std::size_t> pins;
363 std::vector<Row> rows;
364 bool initialized = false;
365};
366
367namespace imm_detail {
368
369/**
370 * True when an inhibitor arc of this mode has reached its threshold.
371 *
372 * A HARD TEST ON THE MEAN, AND A KNOWN WRONG ANSWER WHEN THE MEAN SITS ON THE
373 * THRESHOLD -- a timed mode closes the same indicator smoothly, as
374 * `Phi((thr-m)/sd)` in `petri_theta`; this path does not. See
375 * `_kb/06-solver-catalog.md` for the measurement and for what a real fix costs.
376 */
377inline bool inhibited(const PetriMode& md, const std::vector<double>& x) {
378 for (std::size_t b = 0; b < md.inh_slot.size(); ++b)
379 if (x[md.inh_slot[b]] >= md.inh_thr[b]) return true;
380 return false;
381}
382
383} // namespace imm_detail
384
385/**
386 * An immediate transition has no rate: its fluid limit is a FLOW, an algebraic
387 * unknown pinned by the constraint that its binding input place holds no mass,
388 *
389 * phi_j >= 0, x_b = 0 for the coordinate b that binds mode j
390 *
391 * with the GSPN conflict rule supplying the extra equation when two modes drain
392 * one place: `phi_j*weight_l = phi_l*weight_j` among the enabled modes of highest
393 * firing priority, and `phi = 0` below it.
394 *
395 * THE COUNT IS SQUARE BY CONSTRUCTION: V pins plus (F-V) ratio rows is F
396 * equations for F flows.
397 */
398inline void petri_immediate(const PetriTerms& t, const std::vector<double>& x,
399 PetriImmediate& imm) {
400 const std::size_t n = t.imm_idx.size();
401 if (!imm.initialized) {
402 imm.n = n;
403 imm.active.assign(n, true);
404 imm.bind.assign(n, -1);
405 imm.pins.clear();
406 // An inhibited mode never fires, so it neither carries a flow nor empties
407 // a place. AN EMPTY INPUT PLACE IS NOT A REASON TO DEACTIVATE -- that is
408 // the normal state of an enabled immediate mode, and the whole content of
409 // its pin.
410 for (std::size_t k = 0; k < n; ++k) {
411 const PetriMode& md = t.modes[t.imm_idx[k]];
412 if (md.arc_slot.empty())
413 throw InputError("fluid_petri_immediate: immediate mode " + md.label +
414 " has no enabling arc, so nothing bounds its firing flow and "
415 "the net has no fluid limit. Give it an input place, or make "
416 "it timed");
417 if (imm_detail::inhibited(md, x)) imm.active[k] = false;
418 }
419 imm.initialized = true;
420 }
421
422 // ---- the assignment: each active mode binds the input arc it is shortest of
423 for (std::size_t k = 0; k < n; ++k) {
424 if (!imm.active[k]) {
425 imm.bind[k] = -1;
426 continue;
427 }
428 const PetriMode& md = t.modes[t.imm_idx[k]];
429 if (imm.bind[k] >= 0 &&
430 std::find(md.arc_slot.begin(), md.arc_slot.end(),
431 static_cast<std::size_t>(imm.bind[k])) != md.arc_slot.end())
432 continue; // a binding the caller set explicitly is kept
433 std::size_t best = md.arc_slot[0];
434 double best_lev = x[md.arc_slot[0]] / md.arc_w[0];
435 for (std::size_t a = 1; a < md.arc_slot.size(); ++a) {
436 const double lev = x[md.arc_slot[a]] / md.arc_w[a];
437 if (lev < best_lev) {
438 best_lev = lev;
439 best = md.arc_slot[a];
440 }
441 }
442 imm.bind[k] = static_cast<std::ptrdiff_t>(best);
443 }
444
445 // ---- the equations
446 std::set<std::size_t> pinset;
447 for (std::size_t k = 0; k < n; ++k)
448 if (imm.active[k] && imm.bind[k] >= 0)
449 pinset.insert(static_cast<std::size_t>(imm.bind[k]));
450 imm.pins.assign(pinset.begin(), pinset.end());
451 imm.rows.clear();
452 for (std::size_t p : imm.pins) {
454 row.kind = PetriImmediate::PIN;
455 row.a = p;
456 imm.rows.push_back(row);
457 std::vector<std::size_t> grp;
458 for (std::size_t k = 0; k < n; ++k)
459 if (imm.active[k] && imm.bind[k] == static_cast<std::ptrdiff_t>(p)) grp.push_back(k);
460 if (grp.size() <= 1) continue;
461 int topprio = std::numeric_limits<int>::min();
462 for (std::size_t k : grp) topprio = std::max(topprio, t.modes[t.imm_idx[k]].prio);
463 std::vector<std::size_t> top, low;
464 for (std::size_t k : grp)
465 (t.modes[t.imm_idx[k]].prio == topprio ? top : low).push_back(k);
466 const double w0 = t.modes[t.imm_idx[top[0]]].weight;
467 for (std::size_t i = 1; i < top.size(); ++i) {
470 rr.a = top[0];
471 rr.b = top[i];
472 rr.wa = w0;
473 rr.wb = t.modes[t.imm_idx[top[i]]].weight;
474 imm.rows.push_back(rr);
475 }
476 for (std::size_t k : low) {
479 rr.a = k;
480 imm.rows.push_back(rr);
481 }
482 }
483 for (std::size_t k = 0; k < n; ++k)
484 if (!imm.active[k]) {
487 rr.a = k;
488 imm.rows.push_back(rr);
489 }
490}
491
492// ============================ the refusals ===============================
493
494/** Whether the fluid Petri route can answer this model, and why not. */
496 bool ok = true;
497 std::string reason;
498};
499
500/**
501 * A QUEUEING STATION IS THE ONE STRUCTURAL EXCLUSION: a net whose tokens also
502 * visit a Queue or a Delay is two formalisms at once, and LINE has no reference
503 * semantics for the hand-off.
504 */
505template <class T>
507 PetriVerdict v;
508 const std::size_t I = sn.nodes.size();
509 bool has_transition = false;
510 for (std::size_t i = 0; i < I; ++i)
511 if (sn.nodes[i].nodetype == lang::NodeType::Transition) has_transition = true;
512 if (!has_transition) {
513 v.ok = false;
514 v.reason = "the model has no Transition node, so it is not a Petri net";
515 return v;
516 }
517 for (std::size_t i = 0; i < I; ++i) {
518 const lang::NodeType nt = sn.nodes[i].nodetype;
521 v.ok = false;
522 v.reason = "node " + sn.nodes[i].name + " is a " +
523 std::string(lang::node_type_to_text(nt)) +
524 ". The fluid Petri route solves the marking of a Petri net, and a model "
525 "that also holds queueing stations is two formalisms at once with no "
526 "reference semantics for the hand-off; use SolverCTMC, SolverJMT, "
527 "SolverSSA or SolverLDES";
528 return v;
529 }
530 }
531 // A queueing place declares a service process, which is what turns it into a
532 // station with an embedded queue and a depository.
533 for (std::size_t i = 0; i < I; ++i) {
534 if (sn.nodes[i].nodetype != lang::NodeType::Place) continue;
535 const std::size_t ist = sn.nodes[i].station;
536 if (ist < 1 || ist > sn.nstations) continue;
537 for (std::size_t k = 0; k < sn.nclasses; ++k) {
538 const double rate = num_traits<T>::to_double(sn.rates(ist - 1, k));
539 if (!std::isnan(rate) && rate > 0) {
540 v.ok = false;
541 v.reason = "place " + sn.nodes[i].name +
542 " is a QUEUEING place (it declares a service process), whose embedded "
543 "queue this drift does not carry; use SolverLDES";
544 return v;
545 }
546 }
547 }
548 // -- fluid places (FSPN): a fluid place holds a level, not tokens, so every way of putting a token on it is
549 // refused, and so is a fluid guard an immediate mode carries: a guard on a mode that fires in zero time is
550 // a sliding mode on the guard surface, which a mean-field drift has no value for. The PDMP simulator of
551 // SolverSSA does answer it.
553 if (!fv.ok) {
554 v.ok = false;
555 v.reason = fv.reason;
556 return v;
557 }
558 for (const auto& kv : sn.transparam) {
559 const qn::TransitionParam<T>& tp = kv.second;
560 for (std::size_t m = 0; m < tp.nmodes; ++m) {
561 if (!(m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE)) continue;
562 if (tp.fluid_guards(m).empty()) continue;
563 const std::string mname = (m < tp.modenames.size() && !tp.modenames[m].empty())
564 ? tp.modenames[m]
565 : ("Mode" + std::to_string(m + 1));
566 v.ok = false;
567 v.reason = "immediate mode " + mname + " of transition " + sn.nodes[kv.first - 1].name +
568 " carries a fluid guard, which fires it on the guard surface itself: a sliding mode the "
569 "mean-field drift has no value for; use SolverSSA";
570 return v;
571 }
572 }
573 return v;
574}
575
576// ============================ the driver =================================
577
578/** What the Petri route computes that the station table has no column for. */
581 std::vector<std::string> mode_label;
582 std::vector<double> mode_flow, immediate_flow;
583 std::vector<std::string> invariant_label;
584 std::vector<double> invariant_value, invariant_error;
585 std::vector<std::string> capacity_label;
586 std::vector<std::size_t> capacity_active;
587 std::vector<double> capacity_fraction;
588 std::vector<std::size_t> pinned;
590 /**
591 * MATLAB `moments.petri.fluid`: per fluid place its name, reported level and level variance, its
592 * unthrottled mean drift, the variance rate of its net inflow (filled only by the diffusion refinement),
593 * the bound it is held at (+1 capacity, -1 empty, 0 none), and the `fluid_boundary` mode in force.
594 */
595 struct Fluid {
596 std::vector<std::string> names;
597 std::vector<double> level, level_var, drift, sigma2;
598 std::vector<int> bound;
599 std::string boundary = "reflect";
601};
602
603/** Everything `solver_fluid_petri` returns. */
606 std::vector<double> t;
607 std::vector<std::vector<double>> xvec_t;
608 /** The trajectory tables, indexed [station][class][time point]; filled along `t`. */
609 std::vector<std::vector<std::vector<double>>> QNt, UNt, TNt;
610 /**
611 * The transient marking covariance at each `tvar` instant, over the `cov_idx` coordinates (row-major nc*nc),
612 * filled only when a finite horizon was asked for and the covariance fits `dae_maxcov`.
613 */
614 std::vector<Matrix<double>> Sigmat;
615 std::vector<double> tvar;
616 /** The active-set switches of the transient: (time, event index), MATLAB's `switches`. */
617 std::vector<std::pair<double, int>> switches;
618 std::vector<double> x;
619 std::size_t iters = 0;
620 double resnorm = std::numeric_limits<double>::infinity();
621 bool converged = false;
622 double runtime = 0.0;
625 std::vector<std::string> warnings;
626};
627
628/** Tuning of the outer solve. */
630 std::size_t dae_maxstate = 100;
631 double tol = 1e-8;
632 std::size_t newton_max = 50;
633 /**
634 * `options.config.fluid_boundary`: how a fluid level held at a bound is reported, `reflect` (the bound
635 * itself, the fixed point) or `diffusion` (the mean of a reflected Brownian motion, see `fluid_levels`).
636 */
637 std::string fluid_boundary = "reflect";
638 /**
639 * `options.timespan`: a finite end integrates the transient DAE over [start, end] (MATLAB `local_transient`);
640 * an infinite one reports the seed path, as before.
641 */
642 double timespan_start = 0.0;
643 double timespan_end = std::numeric_limits<double>::infinity();
644 /** `options.tranpoints`: instants added to the integrator's own steps, read off its dense output. */
645 std::vector<double> tranpoints;
646 /** `options.config.dae_maxcov`: the largest covariance block integrated along the trajectory. */
647 std::size_t dae_maxcov = 25;
648};
649
650namespace solver_detail {
651
652/** Everything the residual needs that does not change within one active set. */
653struct Ctx {
654 const PetriTerms* terms = nullptr;
655 const PetriImmediate* imm = nullptr;
656 std::vector<std::size_t> active;
657 const PetriConstraints* con = nullptr;
659 std::vector<double> N;
660 Matrix<double> Dp, Dn;
661};
662
663/**
664 * THE CONSERVATION ROWS A BINDING CAP BREAKS ARE DROPPED: a capped place loses
665 * the tokens that do not fit, so a conserved quantity supported on it is not
666 * conserved while the cap binds, and keeping its row would state an equation the
667 * drift contradicts -- a singular Newton system rather than an inaccuracy.
668 */
669inline Ctx context(const PetriTerms& terms, const PetriConservation& cons,
670 const PetriConstraints& con, const PetriImmediate& imm,
671 const std::vector<std::size_t>& active) {
672 Ctx ctx;
673 ctx.terms = &terms;
674 ctx.imm = &imm;
675 ctx.active = active;
676 ctx.con = &con;
677 Matrix<double> C = cons.C;
678 std::vector<double> N = cons.N;
679 if (!active.empty() && C.rows() > 0) {
680 std::vector<bool> hit(terms.nstate, false);
681 for (std::size_t c : active)
682 for (std::size_t s = 0; s < terms.nstate; ++s)
683 if (con.cover[c][s]) hit[s] = true;
684 std::vector<std::size_t> keep;
685 for (std::size_t r = 0; r < C.rows(); ++r) {
686 bool drop = false;
687 for (std::size_t s = 0; s < terms.nstate && !drop; ++s)
688 if (hit[s] && C(r, s) != 0.0) drop = true;
689 if (!drop) keep.push_back(r);
690 }
691 Matrix<double> Ck(keep.size(), terms.nstate, 0.0);
692 std::vector<double> Nk(keep.size(), 0.0);
693 for (std::size_t i = 0; i < keep.size(); ++i) {
694 for (std::size_t s = 0; s < terms.nstate; ++s) Ck(i, s) = C(keep[i], s);
695 Nk[i] = N[keep[i]];
696 }
697 C = Ck;
698 N = Nk;
699 }
700 ctx.C = C;
701 ctx.N = N;
702 ctx.Dp = Matrix<double>(terms.D.rows(), terms.D.cols(), 0.0);
703 ctx.Dn = Matrix<double>(terms.D.rows(), terms.D.cols(), 0.0);
704 for (std::size_t i = 0; i < terms.D.rows(); ++i)
705 for (std::size_t j = 0; j < terms.D.cols(); ++j) {
706 ctx.Dp(i, j) = std::max(terms.D(i, j), 0.0);
707 ctx.Dn(i, j) = std::min(terms.D(i, j), 0.0);
708 }
709 return ctx;
710}
711
712/** The unknown vector, split into its blocks. */
713struct Unpacked {
714 std::vector<double> x, s2, phi, mu, zeta;
715};
716
717inline Unpacked unpack(const std::vector<double>& u, const PetriTerms& t,
718 const PetriImmediate& imm, std::size_t na) {
719 const std::size_t n = t.nstate, npair = t.npair, ni = imm.n, nl = t.latch_mode.size();
720 Unpacked up;
721 up.x.assign(u.begin(), u.begin() + n);
722 up.s2.assign(u.begin() + n, u.begin() + n + npair);
723 up.phi.assign(u.begin() + n + npair, u.begin() + n + npair + ni);
724 for (std::size_t i = 0; i < up.phi.size(); ++i) up.phi[i] = std::max(0.0, up.phi[i]);
725 up.mu.assign(u.begin() + n + npair + ni, u.begin() + n + npair + ni + nl);
726 up.zeta.assign(u.begin() + n + npair + ni + nl, u.begin() + n + npair + ni + nl + na);
727 for (std::size_t i = 0; i < up.zeta.size(); ++i) up.zeta[i] = std::max(0.0, up.zeta[i]);
728 return up;
729}
730
731inline std::ptrdiff_t index_of(const std::vector<std::size_t>& a, std::size_t v) {
732 for (std::size_t i = 0; i < a.size(); ++i)
733 if (a[i] == v) return static_cast<std::ptrdiff_t>(i);
734 return -1;
735}
736
737/**
738 * The reduction of the fluctuation onto the manifold the fast and clamped
739 * directions leave free.
740 *
741 * AN IMMEDIATE PIN REDUCES OBLIQUELY, ALONG THE FAST REACTION ITSELF, and this
742 * is the one place where the orthogonal projector the queueing twin uses is
743 * WRONG rather than merely different: a slow event depositing into a pinned
744 * place is answered instantly by the immediate transition, so its effective jump
745 * is its own plus the immediate flow it triggers -- the token is forwarded, not
746 * lost. An orthogonal projection deletes the deposit and destroys mass in the
747 * diffusion.
748 *
749 * P = I - Cf * G * E_B, G = G0 * (E_B Cf G0)^-1
750 *
751 * A CAPACITY CAP REDUCES ORTHOGONALLY -- mass that does not fit is genuinely
752 * lost, so there is nothing to forward it to. A SERVER LATCH REDUCES
753 * ORTHOGONALLY TOO, on the LINEARISED row `[-dtheta_j/dm, 1 over the phase
754 * block]`, which is why this projector depends on the iterate.
755 *
756 * THE PSEUDO-INVERSE IS THE RANK-REVEALING ONE, not a regularised normal-equation
757 * solve: the oblique reduction must ANNIHILATE the pinned coordinate exactly, and
758 * a Tikhonov term of 1e-12 leaves it at 1e-12, after which the reduced generator
759 * keeps a marginal eigenvalue and `fluid_lyapunov` refuses the fixed point as
760 * non-hyperbolic -- a failure to reduce reported as a property of the model.
761 */
762inline bool clamp_tangent(const PetriTerms& terms, const PetriImmediate& imm,
763 const PetriConstraints& con, const std::vector<std::size_t>& active,
764 const PetriTheta* th, Matrix<double>& T) {
765 const std::vector<std::size_t>& idx = terms.cov_idx;
766 const std::size_t nc = idx.size();
767 T = Matrix<double>(nc, nc, 0.0);
768 for (std::size_t i = 0; i < nc; ++i) T(i, i) = 1.0;
769
770 std::vector<std::size_t> actk;
771 for (std::size_t k = 0; k < imm.n; ++k)
772 if (imm.active[k] && imm.bind[k] >= 0) actk.push_back(k);
773 const std::vector<std::size_t>& B = imm.pins;
774 if (!actk.empty() && !B.empty()) {
775 Matrix<double> Cf(nc, actk.size(), 0.0);
776 for (std::size_t a = 0; a < actk.size(); ++a) {
777 const std::vector<double>& cv = terms.modes[terms.imm_idx[actk[a]]].cvec;
778 for (std::size_t i = 0; i < nc; ++i) Cf(i, a) = cv[idx[i]];
779 }
780 Matrix<double> G0(actk.size(), B.size(), 0.0);
781 for (std::size_t jb = 0; jb < B.size(); ++jb) {
782 std::vector<std::size_t> grp;
783 double wsum = 0.0;
784 for (std::size_t q = 0; q < actk.size(); ++q)
785 if (imm.bind[actk[q]] == static_cast<std::ptrdiff_t>(B[jb])) {
786 grp.push_back(q);
787 wsum += terms.modes[terms.imm_idx[actk[q]]].weight;
788 }
789 if (grp.empty()) continue;
790 const bool uniform = !(wsum > 0);
791 for (std::size_t q : grp) {
792 const double w =
793 uniform ? 1.0 : terms.modes[terms.imm_idx[actk[q]]].weight;
794 G0(q, jb) = w / (uniform ? static_cast<double>(grp.size()) : wsum);
795 }
796 }
797 Matrix<double> EB(B.size(), nc, 0.0);
798 for (std::size_t jb = 0; jb < B.size(); ++jb) {
799 const std::ptrdiff_t at = index_of(idx, B[jb]);
800 if (at >= 0) EB(jb, static_cast<std::size_t>(at)) = 1.0;
801 }
802 const Matrix<double> Mb = matmul(matmul(EB, Cf), G0);
803 const Matrix<double> corr = matmul(matmul(Cf, matmul(G0, pinv(Mb))), EB);
804 for (std::size_t i = 0; i < nc; ++i)
805 for (std::size_t j = 0; j < nc; ++j) T(i, j) -= corr(i, j);
806 }
807
808 std::vector<std::vector<double>> R;
809 for (std::size_t c : active) {
810 std::vector<double> row(nc, 0.0);
811 for (std::size_t i = 0; i < nc; ++i) row[i] = con.A(c, idx[i]);
812 R.push_back(row);
813 }
814 if (th != nullptr) {
815 for (std::size_t j : terms.latch_mode) {
816 std::vector<double> row(nc, 0.0);
817 for (std::size_t z : terms.modes[j].zblk) {
818 const std::ptrdiff_t at = index_of(idx, z);
819 if (at >= 0) row[static_cast<std::size_t>(at)] = 1.0;
820 }
821 for (std::size_t q = 0; q < th->dslot[j].size(); ++q) {
822 const std::ptrdiff_t at = index_of(idx, th->dslot[j][q]);
823 if (at >= 0) row[static_cast<std::size_t>(at)] -= th->dval[j][q];
824 }
825 R.push_back(row);
826 }
827 }
828 if (!R.empty()) {
829 bool any = false;
830 for (const std::vector<double>& row : R)
831 for (double v : row)
832 if (std::fabs(v) > 1e-14) any = true;
833 if (any) {
834 Matrix<double> Rm(R.size(), nc, 0.0);
835 for (std::size_t i = 0; i < R.size(); ++i)
836 for (std::size_t j = 0; j < nc; ++j) Rm(i, j) = R[i][j];
837 Matrix<double> Rt(nc, R.size(), 0.0);
838 for (std::size_t i = 0; i < R.size(); ++i)
839 for (std::size_t j = 0; j < nc; ++j) Rt(j, i) = Rm(i, j);
840 const Matrix<double> RRt = matmul(Rm, Rt);
841 const Matrix<double> corr = matmul(matmul(Rt, pinv(RRt)), Rm);
842 Matrix<double> P(nc, nc, 0.0);
843 for (std::size_t i = 0; i < nc; ++i)
844 for (std::size_t j = 0; j < nc; ++j) P(i, j) = (i == j ? 1.0 : 0.0) - corr(i, j);
845 T = matmul(P, T);
846 }
847 }
848 double worst = 0.0;
849 for (std::size_t i = 0; i < nc; ++i)
850 for (std::size_t j = 0; j < nc; ++j)
851 worst = std::max(worst, std::fabs(T(i, j) - (i == j ? 1.0 : 0.0)));
852 return worst > 1e-14;
853}
854
855/**
856 * One Lyapunov solve, over the marking coordinates.
857 *
858 * THE DIFFUSION COUNTS THE STOCHASTIC EVENTS ONLY: an immediate flow is not a
859 * Poisson stream with an intensity but the limit of an infinitely fast one whose
860 * fluctuation is slaved, and its pinned coordinate is projected out.
861 */
862inline Matrix<double> sigma_of(const PetriTerms& terms, const Matrix<double>& A,
863 const std::vector<double>& r, const Matrix<double>* clampT) {
864 const std::vector<std::size_t>& idx = terms.cov_idx;
865 const std::size_t nc = idx.size(), ns = terms.stoch_col.size(), nfl = terms.flow_col.size();
866 Matrix<double> Dc(nc, std::max<std::size_t>(ns, 1), 0.0);
867 for (std::size_t i = 0; i < nc; ++i)
868 for (std::size_t j = 0; j < ns; ++j) Dc(i, j) = terms.D(idx[i], terms.stoch_col[j]);
869 Matrix<double> Df(nc, nfl, 0.0);
870 for (std::size_t i = 0; i < nc; ++i)
871 for (std::size_t j = 0; j < nfl; ++j) Df(i, j) = terms.D(idx[i], terms.flow_col[j]);
872 Matrix<double> Am(nc, nc, 0.0);
873 for (std::size_t i = 0; i < nc; ++i)
874 for (std::size_t j = 0; j < nc; ++j) Am(i, j) = A(idx[i], idx[j]);
875 if (clampT != nullptr) {
876 // BOTH the jump directions and the generator are reduced: reducing Dc
877 // alone would fix the subspace but leave the generator's orthogonal
878 // component on it, which is not the reduced dynamics when the reduction
879 // is oblique.
880 Dc = matmul(*clampT, Dc);
881 if (nfl > 0) Df = matmul(*clampT, Df);
882 Am = matmul(*clampT, Am);
883 }
884 Matrix<double> Q(nc, nc, 0.0);
885 for (std::size_t i = 0; i < nc; ++i)
886 for (std::size_t j = 0; j < nc; ++j) {
887 double v = 0.0;
888 for (std::size_t e = 0; e < ns; ++e)
889 v += Dc(i, e) * r[terms.stoch_col[e]] * Dc(j, e);
890 Q(i, j) = v;
891 }
892 // A FLUID LEVEL FLUCTUATES THROUGH THE DRIFT, NOT THROUGH A JUMP OF ITS OWN: its flows carry no noise, so
893 // range(Dc) alone would leave the level out of the reachable subspace and report it as deterministic. The
894 // flow directions are added to the basis, which is where the marking's fluctuation is carried to.
895 Matrix<double> basis(nc, Dc.cols() + nfl, 0.0);
896 for (std::size_t i = 0; i < nc; ++i) {
897 for (std::size_t j = 0; j < Dc.cols(); ++j) basis(i, j) = Dc(i, j);
898 for (std::size_t j = 0; j < nfl; ++j) basis(i, Dc.cols() + j) = Df(i, j);
899 }
900 FluidLyapunovInfo info;
901 const Matrix<double> Sc = fluid_lyapunov(Am, Q, basis, info);
902 Matrix<double> Sigma(terms.nstate, terms.nstate, 0.0);
903 for (std::size_t i = 0; i < nc; ++i)
904 for (std::size_t j = 0; j < nc; ++j) Sigma(idx[i], idx[j]) = Sc(i, j);
905 return Sigma;
906}
907
908/**
909 * The multipliers the binding rows put on the two legs of the drift. A capacity row throttles the DEPOSIT leg of
910 * the coordinates it covers, an empty-level row the WITHDRAWAL leg, each by the fraction zeta that keeps its
911 * place on the bound. Port of `local_gains`.
912 */
913inline void gains(const PetriConstraints& con, const std::vector<std::size_t>& active,
914 const std::vector<double>& zeta, std::size_t n, std::vector<double>& gP,
915 std::vector<double>& gN) {
916 gP.assign(n, 1.0);
917 gN.assign(n, 1.0);
918 for (std::size_t k = 0; k < active.size(); ++k) {
919 const std::size_t c = active[k];
920 const double z = k < zeta.size() ? zeta[k] : 1.0;
921 const bool low = c < con.lower.size() && con.lower[c];
922 for (std::size_t s = 0; s < n; ++s)
923 if (con.cover[c][s]) (low ? gN[s] : gP[s]) *= z;
924 }
925}
926
927/** The stacked residual, the rates it was evaluated at, and the covariance. */
928struct Residual {
929 bool ok = false;
930 std::vector<double> G, r;
931 PetriTheta th;
932 Matrix<double> Sigma;
933};
934
935/**
936 * The coupled algebraic system, stacked.
937 *
938 * `ok` is false when the closure cannot be evaluated at this iterate, so the line
939 * search can back off; the first evaluation of a pass runs with `quiet=false`,
940 * where a genuine failure surfaces.
941 */
942inline Residual residual(const std::vector<double>& u, const Ctx& ctx, bool quiet) {
943 const PetriTerms& terms = *ctx.terms;
944 const PetriImmediate& imm = *ctx.imm;
945 const std::size_t n = terms.nstate;
946 const Unpacked up = unpack(u, terms, imm, ctx.active.size());
947
948 Residual res;
949 res.th = petri_theta(terms, up.x, up.s2);
950 res.r = petri_rates(terms, up.x, up.phi, up.mu, res.th);
951
952 // the deposit gate of every binding capacity, and the withdrawal gate of every binding empty level, as
953 // products of fractions
954 std::vector<double> gP, gN;
955 gains(*ctx.con, ctx.active, up.zeta, n, gP, gN);
956 std::vector<double> drift(n, 0.0);
957 for (std::size_t s = 0; s < n; ++s) {
958 double neg = 0.0, pos = 0.0;
959 for (std::size_t e = 0; e < terms.nev; ++e) {
960 neg += ctx.Dn(s, e) * res.r[e];
961 pos += ctx.Dp(s, e) * res.r[e];
962 }
963 drift[s] = gN[s] * neg + gP[s] * pos;
964 }
965
966 try {
967 const Matrix<double> A = petri_jacobian(terms, res.th);
968 Matrix<double> T;
969 const bool reduced =
970 clamp_tangent(terms, imm, *ctx.con, ctx.active, &res.th, T);
971 res.Sigma = sigma_of(terms, A, res.r, reduced ? &T : nullptr);
972 } catch (const Error&) {
973 if (!quiet) throw;
974 res.ok = false;
975 return res;
976 }
977
978 std::vector<double> G;
979 G.reserve(n + ctx.C.rows() + terms.npair + imm.rows.size() + terms.latch_mode.size() +
980 ctx.active.size());
981 for (double d : drift) G.push_back(d);
982 for (std::size_t r = 0; r < ctx.C.rows(); ++r) {
983 double v = 0.0;
984 for (std::size_t s = 0; s < n; ++s) v += ctx.C(r, s) * up.x[s];
985 G.push_back(v - ctx.N[r]);
986 }
987 for (std::size_t p = 0; p < terms.npair; ++p)
988 G.push_back(up.s2[p] - res.Sigma(terms.cov_pairs[p].first, terms.cov_pairs[p].second));
989 for (const PetriImmediate::Row& row : imm.rows) {
990 if (row.kind == PetriImmediate::PIN) G.push_back(up.x[row.a]);
991 else if (row.kind == PetriImmediate::RATIO)
992 G.push_back(up.phi[row.a] * row.wb - up.phi[row.b] * row.wa);
993 else G.push_back(up.phi[row.a]);
994 }
995 for (std::size_t j : terms.latch_mode) {
996 double s = 0.0;
997 for (std::size_t z : terms.modes[j].zblk) s += up.x[z];
998 G.push_back(s - res.th.theta[j]);
999 }
1000 for (std::size_t k = 0; k < ctx.active.size(); ++k) {
1001 double v = 0.0;
1002 for (std::size_t s = 0; s < n; ++s) v += ctx.con->A(ctx.active[k], s) * up.x[s];
1003 G.push_back(v - ctx.con->b[ctx.active[k]]);
1004 }
1005 res.G = G;
1006 res.ok = true;
1007 return res;
1008}
1009
1010inline double inf_norm(const std::vector<double>& v) {
1011 double m = 0.0;
1012 for (double d : v) m = std::max(m, std::fabs(d));
1013 return m;
1014}
1015
1016/**
1017 * An iterate projected onto its feasible box, the lower bound only.
1018 *
1019 * The bound is a VECTOR, one entry per unknown, never a count: a scalar "nfree"
1020 * is indistinguishable from a one-unknown bound vector, which is how the MATLAB
1021 * twin crashed on the simplest net in the tree.
1022 */
1023inline std::vector<double> project(const std::vector<double>& u, const std::vector<double>& lb) {
1024 if (lb.empty()) return u;
1025 if (lb.size() != u.size())
1026 throw InputError("fluid_petri: the bound vector has " + std::to_string(lb.size()) +
1027 " entries for " + std::to_string(u.size()) + " unknowns");
1028 std::vector<double> w = u;
1029 for (std::size_t i = 0; i < w.size(); ++i)
1030 if (std::isfinite(lb[i])) w[i] = std::max(lb[i], w[i]);
1031 return w;
1032}
1033
1034/** Forward-difference Jacobian of the residual. */
1035inline Matrix<double> fdjac(const Ctx& ctx, const std::vector<double>& u,
1036 const std::vector<double>& G) {
1037 const std::size_t n = u.size(), m = G.size();
1038 Matrix<double> J(std::max<std::size_t>(m, 1), std::max<std::size_t>(n, 1), 0.0);
1039 for (std::size_t k = 0; k < n; ++k) {
1040 const double h = 1e-7 * std::max(1.0, std::fabs(u[k]));
1041 std::vector<double> up = u;
1042 up[k] += h;
1043 Residual rp = residual(up, ctx, true);
1044 if (rp.ok) {
1045 for (std::size_t i = 0; i < m; ++i) J(i, k) = (rp.G[i] - G[i]) / h;
1046 continue;
1047 }
1048 up[k] = u[k] - h;
1049 Residual rm = residual(up, ctx, true);
1050 if (!rm.ok) continue;
1051 for (std::size_t i = 0; i < m; ++i) J(i, k) = (G[i] - rm.G[i]) / h;
1052 }
1053 return J;
1054}
1055
1056struct NewtonResult {
1057 std::vector<double> u;
1058 std::size_t iterations = 0;
1059 bool converged = false;
1060 double resnorm = std::numeric_limits<double>::infinity();
1061};
1062
1063/** Damped projected Newton with a finite-difference Jacobian. */
1064inline NewtonResult newton(const Ctx& ctx, const std::vector<double>& u0, double tol,
1065 std::size_t maxit, const std::vector<double>& lb) {
1066 NewtonResult out;
1067 std::vector<double> u = project(u0, lb);
1068 Residual res = residual(u, ctx, false);
1069 if (!res.ok) {
1070 out.u = u;
1071 return out;
1072 }
1073 std::vector<double> G = res.G;
1074 double resnorm = inf_norm(G);
1075 std::size_t it = 0;
1076 for (it = 1; it <= maxit; ++it) {
1077 if (resnorm <= tol) {
1078 out.u = u;
1079 out.iterations = it - 1;
1080 out.converged = true;
1081 out.resnorm = resnorm;
1082 return out;
1083 }
1084 const Matrix<double> J = fdjac(ctx, u, G);
1085 Matrix<double> rhs(G.size(), 1, 0.0);
1086 for (std::size_t i = 0; i < G.size(); ++i) rhs(i, 0) = -G[i];
1087 const Matrix<double> du = matmul(pinv(J), rhs);
1088 double lam = 1.0;
1089 bool improved = false;
1090 for (int b = 0; b < 30; ++b) {
1091 std::vector<double> un(u.size(), 0.0);
1092 for (std::size_t i = 0; i < u.size(); ++i) un[i] = u[i] + lam * du(i, 0);
1093 un = project(un, lb);
1094 Residual rn = residual(un, ctx, true);
1095 if (rn.ok) {
1096 const double v = inf_norm(rn.G);
1097 if (v < resnorm) {
1098 u = un;
1099 G = rn.G;
1100 resnorm = v;
1101 improved = true;
1102 break;
1103 }
1104 }
1105 lam *= 0.5;
1106 }
1107 if (!improved) break;
1108 }
1109 out.u = u;
1110 out.iterations = it;
1111 out.converged = resnorm <= tol;
1112 out.resnorm = resnorm;
1113 return out;
1114}
1115
1116/**
1117 * The first-order drift: the same rates at zero variance, with an immediate mode
1118 * firing at LAM times its enabling degree and its firing weight, and the server
1119 * latch relaxed at LAM towards the enabling degree instead of solved. Both are
1120 * approximations of an algebraic constraint by a fast reaction, and both are
1121 * confined to the seed.
1122 */
1123inline std::vector<double> seed_drift(const PetriTerms& terms, const std::vector<double>& xin,
1124 double lam) {
1125 std::vector<double> x = xin;
1126 for (std::size_t i = 0; i < x.size(); ++i) x[i] = std::max(0.0, x[i]);
1127 // The inhibitor gate is smoothed here (see petri_theta): a hard step chatters.
1128 const PetriTheta th = petri_theta(
1129 terms, x, std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0), petri_coarse_tol());
1130 std::vector<double> phi(terms.imm_idx.size(), 0.0);
1131 for (std::size_t k = 0; k < phi.size(); ++k) {
1132 const std::size_t j = terms.imm_idx[k];
1133 phi[k] = lam * terms.modes[j].weight * th.theta[j];
1134 }
1135 std::vector<double> mu(terms.latch_mode.size(), 0.0);
1136 for (std::size_t q = 0; q < mu.size(); ++q) {
1137 const std::size_t j = terms.latch_mode[q];
1138 double s = 0.0;
1139 for (std::size_t z : terms.modes[j].zblk) s += x[z];
1140 mu[q] = lam * (th.theta[j] - s);
1141 }
1142 const std::vector<double> r = petri_rates(terms, x, phi, mu, th);
1143 std::vector<double> d(terms.nstate, 0.0);
1144 for (std::size_t s = 0; s < terms.nstate; ++s) {
1145 double v = 0.0;
1146 for (std::size_t e = 0; e < terms.nev; ++e) v += terms.D(s, e) * r[e];
1147 d[s] = v;
1148 }
1149 // A fluid level stops at its bounds: the flow that would carry it past the empty level or the capacity is
1150 // the part the binding multiplier would cut. The bound test reads the UNCLIPPED state, as the reference does.
1151 for (std::size_t q = 0; q < terms.nf; ++q) {
1152 const std::size_t slot = terms.fluid_slot[q];
1153 if ((xin[slot] <= 0.0 && d[slot] < 0.0) || (xin[slot] >= terms.fluid_cap[q] && d[slot] > 0.0))
1154 d[slot] = 0.0;
1155 }
1156 return d;
1157}
1158
1159/**
1160 * The bound rows of the fluid places that the seed ended on, and the refusals of the two passive levels no
1161 * fixed point describes. Port of `local_fluid_seed_active`.
1162 *
1163 * A level that NOTHING READS (no guard, no fluid rate dependence) has a drift column of zeros, so the algebraic
1164 * solve cannot place it: it sits on a bound when its drift points out of the interval, and otherwise it has no
1165 * stationary value at all -- it grows without limit under a positive drift and an infinite capacity, and it
1166 * wanders with no restoring force under a zero drift in the interior. Both are refused by name rather than
1167 * answered with the seed's end.
1168 */
1169inline std::vector<std::size_t> fluid_seed_active(const PetriTerms& terms, const PetriConstraints& con,
1170 const std::vector<double>& x) {
1171 std::vector<std::size_t> active;
1172 if (terms.nf == 0) return active;
1173 std::vector<double> xp = x;
1174 for (std::size_t i = 0; i < xp.size(); ++i) xp[i] = std::max(0.0, xp[i]);
1175 const PetriTheta th = petri_theta(
1176 terms, xp, std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0), petri_coarse_tol());
1177 const std::vector<double> r = petri_rates(terms, xp, std::vector<double>(terms.imm_idx.size(), 0.0),
1178 std::vector<double>(terms.latch_mode.size(), 0.0), th);
1179 // The reference evaluates the seed drift first and then overwrites every fluid entry of it with this free
1180 // drift, so only the free drift is read.
1181 double maxbase = 0.0;
1182 for (double b : terms.rate_base) maxbase = std::max(maxbase, std::fabs(b));
1183 for (std::size_t q = 0; q < terms.nf; ++q) {
1184 const std::size_t slot = terms.fluid_slot[q];
1185 double dfree = 0.0;
1186 for (std::size_t e = 0; e < terms.nev; ++e) dfree += terms.D(slot, e) * r[e];
1187 bool read = false;
1188 for (const PetriMode& md : terms.modes) {
1189 if (std::find(md.guard_slot.begin(), md.guard_slot.end(), slot) != md.guard_slot.end()) read = true;
1190 if (std::find(md.fdep_slot.begin(), md.fdep_slot.end(), slot) != md.fdep_slot.end()) read = true;
1191 }
1192 const double cap = terms.fluid_cap[q];
1193 const double scale = std::max(1.0, std::max(std::fabs(dfree), maxbase));
1194 const double tolx = petri_coarse_tol() * std::max(1.0, std::min(cap, std::max(std::fabs(x[slot]), 1.0)));
1195 const double told = petri_coarse_tol() * scale;
1196 std::ptrdiff_t low_row = -1, up_row = -1;
1197 for (std::size_t c = 0; c < con.b.size(); ++c) {
1198 if (!(con.fluid[c] && con.cover[c][slot])) continue;
1199 if (con.lower[c]) low_row = static_cast<std::ptrdiff_t>(c);
1200 else up_row = static_cast<std::ptrdiff_t>(c);
1201 }
1202 if (x[slot] <= tolx && dfree <= told) {
1203 if (low_row >= 0) active.push_back(static_cast<std::size_t>(low_row));
1204 } else if (std::isfinite(cap) && x[slot] >= cap - tolx && dfree >= -told) {
1205 if (up_row >= 0) active.push_back(static_cast<std::size_t>(up_row));
1206 } else if (!read) {
1207 char buf[64];
1208 std::snprintf(buf, sizeof(buf), "%g", dfree);
1209 if (!std::isfinite(cap) && dfree > told)
1210 throw NumericError(ErrorKind{"Divergent"},
1211 "solver_fluid_petri: Fluid place " + terms.fluid_name[q] + " has a positive mean drift (" +
1212 std::string(buf) + ") and no capacity, so its level grows without bound and has no steady "
1213 "state. Give it a capacity with setFluidCapacity.");
1214 if (std::fabs(dfree) <= told)
1215 throw NumericError(ErrorKind{"Degenerate"},
1216 "solver_fluid_petri: Fluid place " + terms.fluid_name[q] + " has a zero mean drift and "
1217 "nothing reads its level, so its stationary level is undetermined (the level is null "
1218 "recurrent).");
1219 }
1220 }
1221 return active;
1222}
1223
1224/**
1225 * The reported level of every fluid place. Port of `local_fluid_levels`.
1226 *
1227 * Under `reflect` (the default) it is the fixed point: a level on a bound is the bound. Under `diffusion` a
1228 * level on a bound is replaced by the stationary mean of a Brownian motion reflected on [0, B], with the drift
1229 * mu the level would have without the bound and the variance rate sigma2 of its net inflow. The inflow
1230 * fluctuates through its impulses (the jumps delta) and through the marking that drives its flows (gradient g),
1231 * and the long-run variance rate of that integrated linear-noise process is
1232 *
1233 * sigma2 = (delta - g W) diag(r) (delta - g W)', W = V (V' A V)^-1 V' Dc
1234 *
1235 * on the span V of the stochastic jumps Dc of the marking. With kappa = 2 mu / sigma2 the reflected law is
1236 * exponential on [0, B]:
1237 *
1238 * E = B / (1 - exp(-kappa B)) - 1/kappa, Var = 1/kappa^2 - B^2 / (4 sinh^2(kappa B / 2))
1239 *
1240 * and E = -1/kappa, Var = 1/kappa^2 when B = Inf. The refinement is reported, not fed back into the fixed point.
1241 */
1242inline PetriReport::Fluid fluid_levels(const PetriTerms& terms, const PetriConstraints& con,
1243 const PetriImmediate& imm, const std::vector<std::size_t>& active,
1244 const std::vector<double>& x, const std::vector<double>& s2,
1245 const std::vector<double>& phi, const std::vector<double>& r,
1246 const Matrix<double>& Sigma, const std::string& boundary) {
1247 std::string mode = boundary.empty() ? std::string("reflect") : boundary;
1248 for (char& ch : mode) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
1249 if (mode != "reflect" && mode != "diffusion")
1250 throw InputError(ErrorKind{"ConfigInvalid"},
1251 "solver_fluid_petri: options.config.fluid_boundary must be 'reflect' or 'diffusion', "
1252 "got '" + mode + "'.");
1253 const std::size_t nf = terms.nf;
1254 PetriReport::Fluid fl;
1255 fl.names = terms.fluid_name;
1256 fl.level.assign(nf, 0.0);
1257 fl.level_var.assign(nf, 0.0);
1258 fl.drift.assign(nf, 0.0);
1259 fl.sigma2.assign(nf, 0.0);
1260 fl.bound.assign(nf, 0);
1261 fl.boundary = mode;
1262 if (nf == 0) return fl;
1263 bool any_bound = false;
1264 for (std::size_t q = 0; q < nf; ++q) {
1265 const std::size_t slot = terms.fluid_slot[q];
1266 fl.level[q] = x[slot];
1267 fl.level_var[q] = std::max(0.0, Sigma(slot, slot));
1268 double d = 0.0;
1269 for (std::size_t e = 0; e < terms.nev; ++e) d += terms.D(slot, e) * r[e];
1270 fl.drift[q] = d;
1271 for (std::size_t c : active)
1272 if (con.fluid[c] && con.cover[c][slot]) fl.bound[q] = con.lower[c] ? -1 : 1;
1273 if (fl.bound[q] != 0) any_bound = true;
1274 }
1275 if (mode != "diffusion" || !any_bound) return fl;
1276
1277 // the marking that drives the flows, reduced as the covariance is, but with the fluid bounds left out: they
1278 // are what the refinement replaces
1279 const PetriTheta th = petri_theta(terms, x, s2);
1280 const Matrix<double> A = petri_jacobian(terms, th);
1281 (void)phi;
1282 std::vector<std::size_t> tokact;
1283 for (std::size_t c : active)
1284 if (!con.fluid[c]) tokact.push_back(c);
1285 Matrix<double> T;
1286 clamp_tangent(terms, imm, con, tokact, &th, T);
1287 std::vector<bool> isfl(terms.nstate, false);
1288 for (std::size_t q = 0; q < nf; ++q) isfl[terms.fluid_slot[q]] = true;
1289 // tok: positions in cov_idx of the coordinates that are not fluid levels (cov_idx is the identity here)
1290 std::vector<std::size_t> tokpos, tokslot;
1291 for (std::size_t i = 0; i < terms.cov_idx.size(); ++i)
1292 if (!isfl[terms.cov_idx[i]]) {
1293 tokpos.push_back(i);
1294 tokslot.push_back(terms.cov_idx[i]);
1295 }
1296 const std::size_t nt = tokslot.size(), ns = terms.stoch_col.size();
1297 Matrix<double> Ttok(nt, nt, 0.0), Dtok(nt, ns, 0.0), Atok(nt, nt, 0.0);
1298 for (std::size_t i = 0; i < nt; ++i) {
1299 for (std::size_t j = 0; j < nt; ++j) {
1300 Ttok(i, j) = T(tokpos[i], tokpos[j]);
1301 Atok(i, j) = A(tokslot[i], tokslot[j]);
1302 }
1303 for (std::size_t e = 0; e < ns; ++e) Dtok(i, e) = terms.D(tokslot[i], terms.stoch_col[e]);
1304 }
1305 const Matrix<double> Dc = (nt > 0 && ns > 0) ? matmul(Ttok, Dtok) : Matrix<double>(nt, ns, 0.0);
1306 const Matrix<double> Ar = nt > 0 ? matmul(Ttok, Atok) : Matrix<double>(0, 0, 0.0);
1307 const Matrix<double> V = (nt > 0 && ns > 0) ? fluid::detail::fluid_orth(Dc) : Matrix<double>(nt, 0, 0.0);
1308 Matrix<double> W(nt, ns, 0.0);
1309 if (V.cols() > 0) {
1310 const Matrix<double> Vt = V.transpose();
1311 const Matrix<double> VAV = matmul(matmul(Vt, Ar), V);
1312 W = matmul(V, matmul(inverse(VAV), matmul(Vt, Dc)));
1313 }
1314 for (std::size_t q = 0; q < nf; ++q) {
1315 if (fl.bound[q] == 0) continue;
1316 const std::size_t slot = terms.fluid_slot[q];
1317 std::vector<double> h(ns, 0.0);
1318 for (std::size_t e = 0; e < ns; ++e) h[e] = terms.D(slot, terms.stoch_col[e]);
1319 if (V.cols() > 0)
1320 for (std::size_t e = 0; e < ns; ++e) {
1321 double gw = 0.0;
1322 for (std::size_t i = 0; i < nt; ++i) gw += A(slot, tokslot[i]) * W(i, e);
1323 h[e] -= gw;
1324 }
1325 double sig2 = 0.0;
1326 for (std::size_t e = 0; e < ns; ++e) sig2 += h[e] * h[e] * r[terms.stoch_col[e]];
1327 fl.sigma2[q] = sig2;
1328 const double mu = fl.drift[q];
1329 const double B = terms.fluid_cap[q];
1330 if (!(sig2 > 1e-14)) continue; // GlobalConstants.Zero
1331 const double kappa = 2.0 * mu / sig2;
1332 if (!std::isfinite(B)) {
1333 if (kappa >= 0) continue;
1334 fl.level[q] = -1.0 / kappa;
1335 fl.level_var[q] = 1.0 / (kappa * kappa);
1336 } else if (std::fabs(kappa * B) < 1e-8) {
1337 fl.level[q] = B / 2.0;
1338 fl.level_var[q] = B * B / 12.0;
1339 } else {
1340 fl.level[q] = B / (-std::expm1(-kappa * B)) - 1.0 / kappa;
1341 const double sh = std::sinh(kappa * B / 2.0);
1342 fl.level_var[q] = std::max(0.0, 1.0 / (kappa * kappa) - B * B / (4.0 * sh * sh));
1343 }
1344 }
1345 return fl;
1346}
1347
1348// ============================ the transient ==============================
1349
1350/**
1351 * The immediate flows the SEED trajectory carried, point by point: the approximation the seed integrated, and
1352 * therefore the one its throughput table has to be read at.
1353 */
1354inline std::vector<std::vector<double>> seed_flows(const PetriTerms& terms,
1355 const std::vector<std::vector<double>>& xt, double lam) {
1356 const std::size_t ni = terms.imm_idx.size();
1357 std::vector<std::vector<double>> phit(xt.size(), std::vector<double>(ni, 0.0));
1358 if (ni == 0) return phit;
1359 for (std::size_t a = 0; a < xt.size(); ++a) {
1360 const PetriTheta th = petri_theta(
1361 terms, xt[a], std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0), petri_coarse_tol());
1362 for (std::size_t k = 0; k < ni; ++k) {
1363 const std::size_t j = terms.imm_idx[k];
1364 phit[a][k] = lam * terms.modes[j].weight * th.theta[j];
1365 }
1366 }
1367 return phit;
1368}
1369
1370/**
1371 * MATLAB's `A\b`, which the collapse is defined by. A rectangular system takes the BASIC solution of a
1372 * column-pivoted Householder QR (LAPACK xGEQP3, first column of largest norm on a tie): with two immediate modes
1373 * draining one place it routes the whole mass down the first, where a minimum-norm solve would split it. A
1374 * singular square system returns false, the non-finite answer the caller already falls back on.
1375 */
1376inline bool petri_mldivide(std::vector<std::vector<double>> A, std::vector<double> b, std::vector<double>& xs) {
1377 const std::size_t m = A.size();
1378 const std::size_t nn = m == 0 ? 0 : A[0].size();
1379 std::vector<std::size_t> perm(nn);
1380 for (std::size_t j = 0; j < nn; ++j) perm[j] = j;
1381 const std::size_t kmax = std::min(m, nn);
1382 std::vector<double> diagR(kmax, 0.0);
1383 for (std::size_t k = 0; k < kmax; ++k) {
1384 std::size_t p = k;
1385 double best = -1.0;
1386 for (std::size_t j = k; j < nn; ++j) {
1387 double nrm = 0.0;
1388 for (std::size_t i = k; i < m; ++i) nrm += A[i][j] * A[i][j];
1389 if (nrm > best) {
1390 best = nrm;
1391 p = j;
1392 }
1393 }
1394 if (p != k) {
1395 for (std::size_t i = 0; i < m; ++i) std::swap(A[i][k], A[i][p]);
1396 std::swap(perm[k], perm[p]);
1397 }
1398 double alpha = std::sqrt(best);
1399 if (alpha == 0.0) {
1400 diagR[k] = 0.0;
1401 continue;
1402 }
1403 if (A[k][k] > 0) alpha = -alpha;
1404 std::vector<double> v(m, 0.0);
1405 for (std::size_t i = k; i < m; ++i) v[i] = A[i][k];
1406 v[k] -= alpha;
1407 double vv = 0.0;
1408 for (std::size_t i = k; i < m; ++i) vv += v[i] * v[i];
1409 if (vv > 0) {
1410 for (std::size_t j = k; j < nn; ++j) {
1411 double d = 0.0;
1412 for (std::size_t i = k; i < m; ++i) d += v[i] * A[i][j];
1413 d = 2.0 * d / vv;
1414 for (std::size_t i = k; i < m; ++i) A[i][j] -= d * v[i];
1415 }
1416 double d = 0.0;
1417 for (std::size_t i = k; i < m; ++i) d += v[i] * b[i];
1418 d = 2.0 * d / vv;
1419 for (std::size_t i = k; i < m; ++i) b[i] -= d * v[i];
1420 }
1421 diagR[k] = A[k][k];
1422 }
1423 const double tol = kmax > 0 ? static_cast<double>(std::max(m, nn)) * std::numeric_limits<double>::epsilon() *
1424 std::fabs(diagR[0])
1425 : 0.0;
1426 std::size_t rank = 0;
1427 while (rank < kmax && std::fabs(diagR[rank]) > tol) ++rank;
1428 if (m == nn && rank < nn) return false;
1429 std::vector<double> z(nn, 0.0);
1430 for (std::size_t ii = rank; ii-- > 0;) {
1431 double acc = b[ii];
1432 for (std::size_t j = ii + 1; j < rank; ++j) acc -= A[ii][j] * z[j];
1433 z[ii] = acc / A[ii][ii];
1434 }
1435 xs.assign(nn, 0.0);
1436 for (std::size_t j = 0; j < nn; ++j) xs[perm[j]] = z[j];
1437 return true;
1438}
1439
1440/**
1441 * The fluid vanishing-marking collapse: move the initial marking along the immediate incidence columns until
1442 * every pinned place is empty. Every conserved quantity survives it, since a conserved direction annihilates
1443 * those columns. Port of the collapse in `local_transient`.
1444 */
1445inline std::vector<double> collapse(const PetriTerms& terms, const PetriImmediate& imm,
1446 const std::vector<double>& x0) {
1447 std::vector<double> x = x0;
1448 std::vector<std::size_t> act;
1449 for (std::size_t k = 0; k < imm.n; ++k)
1450 if (imm.active[k]) act.push_back(k);
1451 const std::vector<std::size_t>& pins = imm.pins;
1452 if (act.empty() || pins.empty()) return x;
1453 const std::size_t n = terms.nstate;
1454 std::vector<std::vector<double>> Aimm(pins.size(), std::vector<double>(act.size(), 0.0));
1455 std::vector<double> b(pins.size(), 0.0);
1456 for (std::size_t i = 0; i < pins.size(); ++i) {
1457 for (std::size_t a = 0; a < act.size(); ++a)
1458 Aimm[i][a] = terms.modes[terms.imm_idx[act[a]]].cvec[pins[i]];
1459 b[i] = -x0[pins[i]];
1460 }
1461 std::vector<double> s;
1462 if (!petri_mldivide(Aimm, b, s)) return x;
1463 for (double& v : s) {
1464 if (!std::isfinite(v)) return x;
1465 v = std::max(0.0, v);
1466 }
1467 for (std::size_t r = 0; r < n; ++r) {
1468 double v = 0.0;
1469 for (std::size_t a = 0; a < s.size(); ++a) v += terms.modes[terms.imm_idx[act[a]]].cvec[r] * s[a];
1470 x[r] = x0[r] + v;
1471 }
1472 for (std::size_t p : pins) x[p] = 0.0;
1473 for (std::size_t r = 0; r < terms.nm; ++r) x[r] = std::max(0.0, x[r]);
1474 // the collapse moved the marking, so the servers a multi-phase mode has latched moved with it
1475 const PetriTheta th =
1476 petri_theta(terms, x, std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0));
1477 for (std::size_t j : terms.latch_mode) {
1478 const PetriMode& md = terms.modes[j];
1479 for (std::size_t h = 0; h < md.zblk.size(); ++h) x[md.zblk[h]] = th.theta[j] * md.pie[h];
1480 }
1481 return x;
1482}
1483
1484/** The right-hand side of one transient segment, in the layout `petri_transient` documents. */
1485inline std::vector<double> dae_rhs(const double* z, const Ctx& ctx, bool withcov, std::size_t nc) {
1486 const PetriTerms& terms = *ctx.terms;
1487 const PetriImmediate& imm = *ctx.imm;
1488 const PetriConstraints& con = *ctx.con;
1489 const std::size_t n = terms.nstate, nimm = imm.n, nact = ctx.active.size(), nlatch = terms.latch_mode.size();
1490 const std::size_t nvar = n + nimm + nlatch + nact;
1491 const std::vector<double> x(z, z + n), phi(z + n, z + n + nimm), mu(z + n + nimm, z + n + nimm + nlatch),
1492 zeta(z + n + nimm + nlatch, z + nvar);
1493 const PetriTheta th = petri_theta(
1494 terms, x, std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0), 0.0, petri_coarse_tol());
1495 const std::vector<double> r = petri_rates(terms, x, phi, mu, th);
1496 std::vector<double> gP, gN;
1497 gains(con, ctx.active, zeta, n, gP, gN);
1498 std::vector<double> drift(n, 0.0);
1499 for (std::size_t s = 0; s < n; ++s) {
1500 double neg = 0.0, pos = 0.0;
1501 for (std::size_t e = 0; e < terms.nev; ++e) {
1502 neg += ctx.Dn(s, e) * r[e];
1503 pos += ctx.Dp(s, e) * r[e];
1504 }
1505 drift[s] = gN[s] * neg + gP[s] * pos;
1506 }
1507 std::vector<double> dz(nvar + (withcov ? nc * nc : 0), 0.0);
1508 for (std::size_t s = 0; s < n; ++s) dz[s] = drift[s];
1509 for (std::size_t p : imm.pins) dz[p] = x[p]; // the algebraic level pin
1510 std::size_t at = n;
1511 for (const PetriImmediate::Row& row : imm.rows) {
1512 if (row.kind == PetriImmediate::PIN) dz[at++] = drift[row.a];
1513 else if (row.kind == PetriImmediate::RATIO) dz[at++] = phi[row.a] * row.wb - phi[row.b] * row.wa;
1514 else dz[at++] = phi[row.a];
1515 }
1516 // the DIFFERENTIATED latch: the running-server count tracks the enabling degree, so their rates agree
1517 for (std::size_t j : terms.latch_mode) {
1518 double acc = 0.0;
1519 for (std::size_t zc : terms.modes[j].zblk) acc += drift[zc];
1520 for (std::size_t q = 0; q < th.dslot[j].size(); ++q) acc -= th.dval[j][q] * drift[th.dslot[j][q]];
1521 dz[at++] = acc;
1522 }
1523 for (std::size_t k = 0; k < nact; ++k) {
1524 double v = 0.0;
1525 for (std::size_t s = 0; s < n; ++s) v += con.A(ctx.active[k], s) * drift[s];
1526 dz[at++] = v;
1527 }
1528 if (withcov) {
1529 const std::vector<std::size_t>& idx = terms.cov_idx;
1530 const std::vector<std::size_t>& sc = terms.stoch_col;
1531 const Matrix<double> A = petri_jacobian(terms, th);
1532 Matrix<double> Dc(nc, std::max<std::size_t>(sc.size(), 1), 0.0), Am(nc, nc, 0.0);
1533 for (std::size_t i = 0; i < nc; ++i) {
1534 for (std::size_t e = 0; e < sc.size(); ++e) Dc(i, e) = terms.D(idx[i], sc[e]);
1535 for (std::size_t j = 0; j < nc; ++j) Am(i, j) = A(idx[i], idx[j]);
1536 }
1537 Matrix<double> T;
1538 if (clamp_tangent(terms, imm, con, ctx.active, &th, T)) {
1539 Dc = matmul(T, Dc);
1540 Am = matmul(T, Am);
1541 }
1542 Matrix<double> S(nc, nc, 0.0);
1543 for (std::size_t i = 0; i < nc; ++i)
1544 for (std::size_t j = 0; j < nc; ++j) S(i, j) = z[nvar + i * nc + j];
1545 const Matrix<double> AS = matmul(Am, S), SAt = matmul(S, Am.transpose());
1546 for (std::size_t i = 0; i < nc; ++i)
1547 for (std::size_t j = 0; j < nc; ++j) {
1548 double q = 0.0;
1549 for (std::size_t e = 0; e < sc.size(); ++e) q += Dc(i, e) * r[sc[e]] * Dc(j, e);
1550 dz[nvar + i * nc + j] = AS(i, j) + SAt(i, j) + q;
1551 }
1552 }
1553 return dz;
1554}
1555
1556/**
1557 * Where a segment ends: a flow that would go negative, a capacity that starts or stops binding, or a marking
1558 * coordinate that would go negative. A crossing is a value going from positive to non-positive.
1559 */
1560inline std::vector<double> event_values(const double* z, const Ctx& ctx) {
1561 const PetriTerms& terms = *ctx.terms;
1562 const PetriImmediate& imm = *ctx.imm;
1563 const PetriConstraints& con = *ctx.con;
1564 const std::size_t n = terms.nstate, nimm = imm.n, nlatch = terms.latch_mode.size(), ncon = con.b.size();
1565 std::vector<double> val(nimm + ncon + terms.nm, 0.0);
1566 for (std::size_t k = 0; k < nimm; ++k) val[k] = imm.active[k] ? z[n + k] : 1.0;
1567 for (std::size_t c = 0; c < ncon; ++c) {
1568 const std::ptrdiff_t k = index_of(ctx.active, c);
1569 if (k < 0) {
1570 double ax = 0.0;
1571 for (std::size_t s = 0; s < n; ++s) ax += con.A(c, s) * z[s];
1572 val[nimm + c] = con.b[c] - ax;
1573 } else {
1574 val[nimm + c] = 1.0 - z[n + nimm + nlatch + static_cast<std::size_t>(k)];
1575 }
1576 }
1577 for (std::size_t s = 0; s < terms.nm; ++s)
1578 val[nimm + ncon + s] = index_of(imm.pins, s) >= 0 ? 1.0 : z[s] + petri_fine_tol();
1579 return val;
1580}
1581
1582/** One segment's integration state, reached by the RODAS callbacks through `petri_tran_active`. */
1583struct PetriTranSeg {
1584 const Ctx* ctx = nullptr;
1585 bool withcov = false;
1586 std::size_t nc = 0, nz = 0;
1587 std::vector<double> diag, gprev;
1588 const std::vector<double>* req = nullptr;
1589 std::size_t jreq = 0;
1590 std::vector<double> segT;
1591 std::vector<std::vector<double>> segZ;
1592 int hit = -1;
1593};
1594inline PetriTranSeg*& petri_tran_active() {
1595 static PetriTranSeg* p = nullptr;
1596 return p;
1597}
1598
1599inline int petri_tran_fcn(rodas_impl::integer*, rodas_impl::doublereal*, rodas_impl::doublereal* y,
1600 rodas_impl::doublereal* f, rodas_impl::doublereal*, rodas_impl::integer*) {
1601 PetriTranSeg& S = *petri_tran_active();
1602 const std::vector<double> d = dae_rhs(y, *S.ctx, S.withcov, S.nc);
1603 for (std::size_t i = 0; i < S.nz; ++i) f[i] = d[i];
1604 return 0;
1605}
1606inline int petri_tran_jac(rodas_impl::integer*, rodas_impl::doublereal*, rodas_impl::doublereal*,
1607 rodas_impl::doublereal*, rodas_impl::integer*, rodas_impl::doublereal*,
1608 rodas_impl::integer*) { return 0; }
1609inline int petri_tran_dfx(rodas_impl::integer*, rodas_impl::doublereal*, rodas_impl::doublereal*,
1610 rodas_impl::doublereal*, rodas_impl::doublereal*, rodas_impl::integer*) { return 0; }
1611/** The singular mass matrix: banded with MLMAS=MUMAS=0, i.e. its diagonal. */
1612inline int petri_tran_mas(rodas_impl::integer* n, rodas_impl::doublereal* am, rodas_impl::integer* lmas,
1613 rodas_impl::doublereal*, rodas_impl::integer*) {
1614 PetriTranSeg& S = *petri_tran_active();
1615 for (int j = 0; j < *n; ++j) am[0 + j * (*lmas)] = S.diag[static_cast<std::size_t>(j)];
1616 return 0;
1617}
1618inline std::vector<double> petri_tran_dense(double tq, rodas_impl::doublereal* cont, rodas_impl::integer* lrc,
1619 std::size_t nz) {
1620 std::vector<double> y(nz, 0.0);
1621 for (std::size_t i = 0; i < nz; ++i) {
1622 rodas_impl::integer ii = static_cast<rodas_impl::integer>(i + 1);
1623 double t = tq;
1624 y[i] = rodas_impl::contro_(&ii, &t, cont, lrc);
1625 }
1626 return y;
1627}
1628/** The requested instants (`tranpoints`) the step (xold, xend) passed, read off its interpolant. */
1629inline void petri_tran_emit(PetriTranSeg& S, double xold, double xend, rodas_impl::doublereal* cont,
1630 rodas_impl::integer* lrc) {
1631 const std::vector<double>& req = *S.req;
1632 while (S.jreq < req.size() && req[S.jreq] < xend) {
1633 const double tq = req[S.jreq];
1634 if (tq > xold && (S.segT.empty() || tq > S.segT.back())) {
1635 S.segT.push_back(tq);
1636 S.segZ.push_back(petri_tran_dense(tq, cont, lrc, S.nz));
1637 }
1638 ++S.jreq;
1639 }
1640}
1641/**
1642 * Every accepted step is recorded, the requested instants are added off the dense output, and a crossing is
1643 * located on that same interpolant. THE OVERSHOOTING STEP IS NOT REPORTED. The first call precedes any step
1644 * (NR = 1, XOLD = X = t0), where `cont` holds no coefficients yet.
1645 */
1646inline int petri_tran_solout(rodas_impl::integer* nr, rodas_impl::doublereal* xold, rodas_impl::doublereal* x,
1647 rodas_impl::doublereal* y, rodas_impl::doublereal* cont, rodas_impl::integer* lrc,
1648 rodas_impl::integer*, rodas_impl::doublereal*, rodas_impl::integer*,
1649 rodas_impl::integer* irtrn) {
1650 PetriTranSeg& S = *petri_tran_active();
1651 const std::vector<double> gcur = event_values(y, *S.ctx);
1652 int cross = -1;
1653 for (std::size_t c = 0; c < gcur.size(); ++c)
1654 if (S.gprev[c] > 0 && gcur[c] <= 0) {
1655 cross = static_cast<int>(c);
1656 break;
1657 }
1658 S.gprev = gcur;
1659 if (cross < 0) {
1660 if (*nr > 1) petri_tran_emit(S, *xold, *x, cont, lrc);
1661 S.segT.push_back(*x);
1662 S.segZ.push_back(std::vector<double>(y, y + S.nz));
1663 return 0;
1664 }
1665 double lo = *xold, hi = *x;
1666 for (int bit = 0; bit < 60; ++bit) {
1667 const double mid = 0.5 * (lo + hi);
1668 const std::vector<double> ymid = petri_tran_dense(mid, cont, lrc, S.nz);
1669 if (event_values(ymid.data(), *S.ctx)[static_cast<std::size_t>(cross)] > 0) lo = mid;
1670 else hi = mid;
1671 if (hi - lo <= 1e-12 * std::max(1.0, std::fabs(hi))) break;
1672 }
1673 S.hit = cross;
1674 petri_tran_emit(S, *xold, hi, cont, lrc);
1675 S.segT.push_back(hi);
1676 S.segZ.push_back(petri_tran_dense(hi, cont, lrc, S.nz));
1677 // RODAS reads the stop through IRTRN, not through the return value
1678 if (irtrn) *irtrn = -1;
1679 return 0;
1680}
1681
1682/** What `petri_transient` returns. */
1683struct PetriTransient {
1684 std::vector<double> t;
1685 std::vector<std::vector<double>> xt, phit;
1686 std::vector<Matrix<double>> Sigmat;
1687 std::vector<double> tvar;
1688 std::vector<std::pair<double, int>> switches;
1689};
1690
1691/**
1692 * The transient closure as an index-1 DAE with a SINGULAR mass matrix, integrated by RODAS segment by segment
1693 * between active-set switches. Port of `local_transient` in `matlab/src/solvers/FLD/solver_fluid_petri.m`.
1694 *
1695 * THE INITIAL MARKING JUMPS: an immediate transition fires in zero time, so the trajectory starts at the
1696 * collapsed marking. Per segment the unknown is [x; phi; mu; zeta; vec(S)]: a free coordinate integrates its
1697 * drift; a pinned one carries both `x_b = 0` and the differentiated pin `drift_b = 0`, which determines the flow;
1698 * the conflict rule, the differentiated server latch and the differentiated binding rows are algebraic; and the
1699 * covariance `dS = A S + S A' + Q` rides along when it fits `dae_maxcov`.
1700 *
1701 * @param imm the steady-state immediate set, copied and moved here
1702 */
1703inline PetriTransient petri_transient(const PetriTerms& terms, const PetriConservation& cons,
1704 const PetriConstraints& con, PetriImmediate imm,
1705 const std::vector<std::size_t>& active_ss, const PetriOptions& opt,
1706 const std::vector<double>& phiss, const std::vector<double>& zetass,
1707 std::vector<std::string>& warnings) {
1708 using namespace rodas_impl;
1709 const std::size_t n = terms.nstate, nimm = imm.n, nlatch = terms.latch_mode.size(), ncon = con.b.size();
1710 const double tEnd = opt.timespan_end;
1711 const double tStart = std::isfinite(opt.timespan_start) ? opt.timespan_start : 0.0;
1712 const std::size_t maxcov = opt.dae_maxcov > 0 ? opt.dae_maxcov : 25;
1713 const std::size_t nc = terms.cov_idx.size();
1714 const bool withcov = nc > 0 && nc <= maxcov;
1715 if (!withcov && nc > maxcov)
1716 warnings.push_back("The transient covariance would add " + std::to_string(nc * nc) +
1717 " differential states, above the limit of " + std::to_string(maxcov * maxcov) +
1718 " set by options.config.dae_maxcov. Integrating the mean as a DAE with the variance "
1719 "held at its stationary value.");
1720
1721 const std::vector<double> x0 = collapse(terms, imm, terms.x0);
1722
1723 // THE FLUID BOUNDS ARE RE-READ AT THE INITIAL LEVEL, not taken from the steady state: a bound the initial
1724 // level sits on starts binding unthrottled, and the events release it when it should.
1725 std::vector<std::size_t> active;
1726 std::vector<double> zeta;
1727 for (std::size_t k = 0; k < active_ss.size(); ++k) {
1728 const std::size_t c = active_ss[k];
1729 if (!con.fluid[c]) {
1730 active.push_back(c);
1731 zeta.push_back(k < zetass.size() ? zetass[k] : 1.0);
1732 }
1733 }
1734 for (std::size_t q = 0; q < terms.nf; ++q) {
1735 const std::size_t slot = terms.fluid_slot[q];
1736 for (std::size_t c = 0; c < ncon; ++c) {
1737 if (!con.fluid[c] || !con.cover[c][slot]) continue;
1738 if ((x0[slot] <= 0 && con.lower[c]) ||
1739 (x0[slot] > 0 && x0[slot] >= terms.fluid_cap[q] && !con.lower[c])) {
1740 active.push_back(c);
1741 zeta.push_back(1.0);
1742 break;
1743 }
1744 }
1745 }
1746
1747 std::vector<double> req = opt.tranpoints;
1748 std::sort(req.begin(), req.end());
1749 PetriTransient out;
1750 double tcur = tStart;
1751 std::vector<double> xcur = x0, phi = phiss, mu(nlatch, 0.0), Scur(withcov ? nc * nc : 0, 0.0);
1752 const std::size_t segMax = std::max<std::size_t>(8, 4 * (nimm + ncon + 1));
1753 for (std::size_t seg = 0; seg < segMax; ++seg) {
1754 // THE ACTIVE SET IS RE-READ EVERY SEGMENT: a located crossing may have added or released a row.
1755 const Ctx ctx = context(terms, cons, con, imm, active);
1756 const std::size_t nact = ctx.active.size();
1757 const std::size_t nvar = n + nimm + nlatch + nact;
1758 const std::size_t nz = nvar + (withcov ? nc * nc : 0);
1759 PetriTranSeg S;
1760 S.ctx = &ctx;
1761 S.withcov = withcov;
1762 S.nc = nc;
1763 S.nz = nz;
1764 S.diag.assign(nz, 0.0);
1765 for (std::size_t s = 0; s < n; ++s) S.diag[s] = 1.0;
1766 for (std::size_t p : imm.pins) S.diag[p] = 0.0;
1767 for (std::size_t i = nvar; i < nz; ++i) S.diag[i] = 1.0;
1768 std::vector<doublereal> y(nz, 0.0);
1769 std::copy(xcur.begin(), xcur.end(), y.begin());
1770 std::copy(phi.begin(), phi.end(), y.begin() + n);
1771 std::copy(mu.begin(), mu.end(), y.begin() + n + nimm);
1772 std::copy(zeta.begin(), zeta.end(), y.begin() + n + nimm + nlatch);
1773 if (withcov) std::copy(Scur.begin(), Scur.end(), y.begin() + nvar);
1774 S.gprev = event_values(y.data(), ctx);
1775 S.req = &req;
1776 while (S.jreq < req.size() && req[S.jreq] <= tcur) ++S.jreq;
1777
1778 // LWORK per rodas.f, N*(LJAC+LMAS+LE1+14)+20 with a full Jacobian and a diagonal mass matrix
1779 const integer N = static_cast<integer>(nz);
1780 const std::size_t lwork = nz * (nz + 1 + nz + 14) + 20 + 32, liwork = nz + 20 + 32;
1781 std::vector<doublereal> work(lwork, 0.0), rpar(1, 0.0);
1782 std::vector<integer> iwork(liwork, 0), ipar(1, 0);
1783 doublereal rtol = petri_coarse_tol(), atol = petri_fine_tol(), x = tcur, xend = tEnd, h = 1e-6;
1784 integer itol = 0, ifcn = 1, ijac = 0, mljac = N, mujac = N, idfx = 0, imas = 1, mlmas = 0, mumas = 0;
1785 integer iout = 1, idid = 0, lw = static_cast<integer>(lwork), liw = static_cast<integer>(liwork);
1786 bool failed = false;
1787 petri_tran_active() = &S;
1788 try {
1789 rodas_(const_cast<integer*>(&N), (U_fp)petri_tran_fcn, &ifcn, &x, y.data(), &xend, &h, &rtol, &atol,
1790 &itol, (U_fp)petri_tran_jac, &ijac, &mljac, &mujac, (U_fp)petri_tran_dfx, &idfx,
1791 (U_fp)petri_tran_mas, &imas, &mlmas, &mumas, (U_fp)petri_tran_solout, &iout, work.data(), &lw,
1792 iwork.data(), &liw, rpar.data(), ipar.data(), &idid);
1793 if (idid < 0 && S.hit < 0) {
1794 warnings.push_back("The transient DAE failed (RODAS returned idid=" + std::to_string(idid) +
1795 "); the trajectory stops at the last accepted point.");
1796 failed = true;
1797 }
1798 } catch (const Error& ex) {
1799 warnings.push_back(std::string("The transient DAE failed (") + ex.what() +
1800 "); the trajectory stops at the last accepted point.");
1801 failed = true;
1802 }
1803 petri_tran_active() = nullptr;
1804 if (!S.segT.empty()) {
1805 for (std::size_t a = 0; a < S.segT.size(); ++a) {
1806 const std::vector<double>& za = S.segZ[a];
1807 out.t.push_back(S.segT[a]);
1808 out.xt.push_back(std::vector<double>(za.begin(), za.begin() + n));
1809 out.phit.push_back(std::vector<double>(za.begin() + n, za.begin() + n + nimm));
1810 if (withcov) {
1811 out.tvar.push_back(S.segT[a]);
1812 Matrix<double> Sg(nc, nc, 0.0);
1813 for (std::size_t i = 0; i < nc; ++i)
1814 for (std::size_t j = 0; j < nc; ++j) Sg(i, j) = za[nvar + i * nc + j];
1815 out.Sigmat.push_back(Sg);
1816 }
1817 }
1818 tcur = S.segT.back();
1819 const std::vector<double>& zend = S.segZ.back();
1820 xcur.assign(zend.begin(), zend.begin() + n);
1821 phi.assign(zend.begin() + n, zend.begin() + n + nimm);
1822 mu.assign(zend.begin() + n + nimm, zend.begin() + n + nimm + nlatch);
1823 zeta.assign(zend.begin() + n + nimm + nlatch, zend.begin() + nvar);
1824 if (withcov) Scur.assign(zend.begin() + nvar, zend.begin() + nz);
1825 }
1826 if (failed || S.hit < 0 || tcur >= tEnd - petri_fine_tol()) break;
1827 // the active-set move the located crossing asks for
1828 bool changed = false;
1829 const std::size_t ev = static_cast<std::size_t>(S.hit);
1830 if (ev < nimm) {
1831 if (imm.active[ev]) {
1832 imm.active[ev] = false;
1833 petri_immediate(terms, xcur, imm);
1834 changed = true;
1835 }
1836 } else if (ev - nimm < ncon) {
1837 const std::size_t c = ev - nimm;
1838 const std::ptrdiff_t k = index_of(active, c);
1839 if (k < 0) {
1840 active.push_back(c);
1841 zeta.push_back(1.0);
1842 } else {
1843 active.erase(active.begin() + k);
1844 zeta.erase(zeta.begin() + k);
1845 }
1846 changed = true;
1847 } else {
1848 const std::size_t s = ev - nimm - ncon;
1849 for (std::size_t k = 0; k < nimm; ++k) {
1850 const PetriMode& md = terms.modes[terms.imm_idx[k]];
1851 if (imm.active[k] && std::find(md.arc_slot.begin(), md.arc_slot.end(), s) != md.arc_slot.end() &&
1852 imm.bind[k] != static_cast<std::ptrdiff_t>(s)) {
1853 imm.bind[k] = static_cast<std::ptrdiff_t>(s);
1854 petri_immediate(terms, xcur, imm);
1855 changed = true;
1856 break;
1857 }
1858 }
1859 }
1860 out.switches.push_back(std::make_pair(tcur, S.hit));
1861 if (!changed) break;
1862 }
1863 if (out.t.empty()) {
1864 out.t.assign(1, tStart);
1865 out.xt.assign(1, x0);
1866 out.phit.assign(1, phiss);
1867 }
1868 return out;
1869}
1870
1871/**
1872 * The station table along a trajectory, in the conventions of the steady-state table. The rates are read at the
1873 * STATIONARY variance and at the trajectory's own immediate flows; the latch flow moves no marking, so it cannot
1874 * change a place throughput.
1875 */
1876inline void metrics_t(const PetriTerms& terms, const std::vector<std::vector<double>>& xt,
1877 const std::vector<double>& s2, const std::vector<std::vector<double>>& phit, std::size_t M,
1878 std::size_t K, PetriSolution& out) {
1879 const std::size_t nt = xt.size();
1880 out.QNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
1881 out.UNt = out.QNt;
1882 out.TNt = out.QNt;
1883 std::vector<std::vector<double>> Rt(nt);
1884 for (std::size_t a = 0; a < nt; ++a) {
1885 const PetriTheta th = petri_theta(terms, xt[a], s2);
1886 Rt[a] = petri_rates(terms, xt[a], a < phit.size() ? phit[a] : std::vector<double>(), std::vector<double>(),
1887 th);
1888 }
1889 for (std::size_t s = 0; s < terms.nm; ++s) {
1890 if (terms.coord_station[s] < 0 || static_cast<std::size_t>(terms.coord_station[s]) >= M) continue;
1891 const std::size_t i = static_cast<std::size_t>(terms.coord_station[s]), k = terms.coord_class[s];
1892 for (std::size_t a = 0; a < nt; ++a) {
1893 out.QNt[i][k][a] += xt[a][s];
1894 out.UNt[i][k][a] = out.QNt[i][k][a];
1895 }
1896 }
1897 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it = terms.consumers.begin();
1898 it != terms.consumers.end(); ++it) {
1899 const std::size_t i = it->first / K, k = it->first % K;
1900 const std::vector<double>& w = terms.consumer_w.at(it->first);
1901 for (std::size_t a = 0; a < nt; ++a)
1902 for (std::size_t q = 0; q < it->second.size(); ++q) out.TNt[i][k][a] += w[q] * Rt[a][it->second[q]];
1903 }
1904 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it = terms.producers.begin();
1905 it != terms.producers.end(); ++it) {
1906 const std::size_t i = it->first / K, k = it->first % K;
1907 for (std::size_t a = 0; a < nt; ++a)
1908 for (std::size_t e : it->second) out.TNt[i][k][a] += Rt[a][e];
1909 }
1910 // Along a trajectory the empty-level gate is read off the level itself: at an empty place the outflow is cut
1911 // to the inflow, which is the value the binding multiplier takes at that instant.
1912 for (std::size_t q = 0; q < terms.nf; ++q) {
1913 if (terms.fluid_station[q] < 0 || static_cast<std::size_t>(terms.fluid_station[q]) >= M || K == 0) continue;
1914 const std::size_t i = static_cast<std::size_t>(terms.fluid_station[q]), slot = terms.fluid_slot[q];
1915 double levmax = 0.0;
1916 for (std::size_t a = 0; a < nt; ++a) levmax = std::max(levmax, std::fabs(xt[a][slot]));
1917 const double tol_empty = petri_coarse_tol() * std::max(1.0, levmax);
1918 for (std::size_t a = 0; a < nt; ++a) {
1919 const double lev = xt[a][slot];
1920 out.QNt[i][0][a] = lev;
1921 out.UNt[i][0][a] = std::isfinite(terms.fluid_cap[q]) ? lev / terms.fluid_cap[q] : lev;
1922 double outf = 0.0, inf = 0.0;
1923 for (std::size_t e = 0; e < terms.nev; ++e) {
1924 const double d = terms.D(slot, e);
1925 if (d < 0) outf -= d * Rt[a][e];
1926 else if (d > 0) inf += d * Rt[a][e];
1927 }
1928 if (lev <= tol_empty) outf = std::min(outf, inf);
1929 out.TNt[i][0][a] = outf;
1930 }
1931 }
1932}
1933
1934} // namespace solver_detail
1935
1936/**
1937 * Fluid analysis of a stochastic Petri net.
1938 *
1939 * @param sn a model whose nodes are Places, Transitions, Sources and Sinks only
1940 * @param opt the state-size cap, the Newton tolerance and its iteration cap
1941 */
1942template <class T>
1944 const PetriOptions& opt = PetriOptions()) {
1945 const std::size_t M = sn.nstations, K = sn.nclasses;
1946
1947 const PetriVerdict v = petri_applicable(sn);
1948 if (!v.ok)
1949 throw UnsupportedError(ErrorKind{"Model"}, "solver_fluid_petri: the fluid Petri route cannot solve this "
1950 "model: " + v.reason + ".");
1951
1952 const PetriTerms terms = petri_build_terms(sn);
1953 const std::size_t n = terms.nstate, npair = terms.npair;
1954 if (n > opt.dae_maxstate)
1955 throw UnsupportedError(ErrorKind{"StateSpace"},
1956 "solver_fluid_petri: the fluid Petri route solves a " + std::to_string(n) +
1957 "-unknown algebraic system with a finite-difference Jacobian, above the limit of " +
1958 std::to_string(opt.dae_maxstate) +
1959 " set by options.config.dae_maxstate. Raise that limit, or use SolverSSA for a net "
1960 "of this size");
1961
1962 const PetriConservation cons = petri_conservation(terms);
1963 if (cons.leak > 1e-7)
1964 throw NumericError(ErrorKind{"Degenerate"}, "solver_fluid_petri: the conserved directions and the jump matrix "
1965 "disagree: the largest leak per unit rate is " +
1966 std::to_string(cons.leak) + ", where it must be zero");
1967 const PetriConstraints con = petri_constraints(sn, terms);
1968 const std::size_t ncon = con.b.size();
1969
1970 // ---- seed ---------------------------------------------------------------
1971 // Newton needs a point in the basin, not an answer. The immediate modes get a
1972 // large FINITE rate here and only here, scaled to the model's own timescale
1973 // rather than taken from GlobalConstants.Immediate: 1e8 against a rate of
1974 // order one is a stiffness the seed does not need, and the answer does not
1975 // depend on the seed's accuracy.
1976 double rmax = 0.0;
1977 for (std::size_t j : terms.timed_idx) {
1978 double s = 0.0;
1979 for (double d : terms.modes[j].d1) s += d;
1980 rmax = std::max(rmax, s);
1981 }
1982 for (std::size_t e = 0; e < terms.nev; ++e)
1983 if (terms.ev_kind[e] == 3 || terms.ev_kind[e] == 6) rmax = std::max(rmax, terms.rate_base[e]);
1984 if (!(rmax > 0)) rmax = 1.0;
1985 const double lam = std::min(1e8, 1e4 * rmax);
1986 double mass = 0.0;
1987 for (std::size_t s = 0; s < terms.nm; ++s) mass += terms.x0[s];
1988 // A fluid level has to reach its bound or its balance within the horizon too, so its initial level and any
1989 // finite capacity count as mass of the same units.
1990 for (std::size_t q = 0; q < terms.nf; ++q) {
1991 mass += terms.fluid_init[q];
1992 if (std::isfinite(terms.fluid_cap[q])) mass += terms.fluid_cap[q];
1993 }
1994 double Thor = 50.0 * (mass + 1.0) / rmax;
1995
1996 const auto drift_fn = [&terms, lam](const double&, const std::vector<double>& y) {
1997 return solver_detail::seed_drift(terms, y, lam);
1998 };
1999 std::vector<double> tseed;
2000 std::vector<std::vector<double>> xseed;
2001 for (int attempt = 0; attempt < 6; ++attempt) {
2003 oo.rtol = 1e-6;
2004 oo.atol = 1e-10;
2005 oo.store_trajectory = true;
2006 oo.max_steps = 200000;
2007 const OdeSolution<double> sol =
2008 ode_rosenbrock4(drift_fn, 0.0, Thor, terms.x0, oo);
2009 tseed = sol.t;
2010 xseed = sol.y;
2011 const std::vector<double> d = solver_detail::seed_drift(terms, xseed.back(), lam);
2012 if (solver_detail::inf_norm(d) <= 1e-6 * std::max(1.0, rmax * (mass + 1.0))) break;
2013 Thor *= 4.0;
2014 }
2015 std::vector<double> x = xseed.back();
2016
2017 PetriImmediate imm;
2018 petri_immediate(terms, x, imm);
2019 const std::size_t nimm = imm.n;
2020
2021 // THE VARIANCE IS SEEDED POSITIVE: sigma2 = 0 is where min() has no
2022 // derivative, and a saturated net's first-order fixed point sits there.
2023 std::vector<double> s2(npair, 0.0);
2024 std::vector<bool> ondiag(npair, false);
2025 for (std::size_t p = 0; p < npair; ++p) {
2026 ondiag[p] = (terms.cov_pairs[p].first == terms.cov_pairs[p].second);
2027 if (ondiag[p]) s2[p] = std::max(petri_fine_tol(), x[terms.cov_pairs[p].first]);
2028 }
2029
2030 // ---- steady state -------------------------------------------------------
2031 // A FLUID BOUND IS READ OFF THE SEED. A level whose flows do not depend on it has a constant column of drift,
2032 // so a Newton step cannot move it onto a bound; the seed has already run it there, and a bound it presses
2033 // against is binding.
2034 std::vector<std::size_t> active = solver_detail::fluid_seed_active(terms, con, x);
2035 std::vector<double> phi(nimm, 0.0);
2036 const std::size_t nlatch = terms.latch_mode.size();
2037 std::vector<double> mu(nlatch, 0.0), zeta;
2038 std::size_t iters = 0;
2039 const std::size_t aset_max = std::max<std::size_t>(6, 2 * (ncon + nimm) + 2);
2040 bool converged = false;
2041 double resnorm = std::numeric_limits<double>::infinity();
2042 bool have_best = false;
2043 std::vector<double> bx, bs2, bphi, bmu, bzeta;
2044 std::vector<std::size_t> bactive;
2045 PetriImmediate bimm;
2046 double bres = std::numeric_limits<double>::infinity();
2047 bool bconv = false;
2048
2049 for (std::size_t sweep = 0; sweep < aset_max; ++sweep) {
2050 const solver_detail::Ctx ctx =
2051 solver_detail::context(terms, cons, con, imm, active);
2052 std::vector<double> u0;
2053 u0.insert(u0.end(), x.begin(), x.end());
2054 u0.insert(u0.end(), s2.begin(), s2.end());
2055 u0.insert(u0.end(), phi.begin(), phi.end());
2056 u0.insert(u0.end(), mu.begin(), mu.end());
2057 u0.insert(u0.end(), active.size(), 1.0);
2058 std::vector<double> lb(u0.size(), -std::numeric_limits<double>::infinity());
2059 for (std::size_t k = 0; k < nimm; ++k) lb[n + npair + k] = 0.0;
2060 for (std::size_t k = 0; k < active.size(); ++k)
2061 lb[n + npair + nimm + nlatch + k] = 0.0;
2062 for (std::size_t p = 0; p < npair; ++p)
2063 if (ondiag[p]) lb[n + p] = 0.0;
2064
2065 const solver_detail::NewtonResult nr =
2066 solver_detail::newton(ctx, u0, opt.tol, opt.newton_max, lb);
2067 iters += nr.iterations;
2068 resnorm = nr.resnorm;
2069 converged = nr.converged;
2070 const solver_detail::Unpacked up =
2071 solver_detail::unpack(nr.u, terms, imm, active.size());
2072 x = up.x;
2073 s2 = up.s2;
2074 phi = up.phi;
2075 mu = up.mu;
2076 zeta = up.zeta;
2077
2078 if (!have_best || resnorm < bres) {
2079 bx = x; bs2 = s2; bphi = phi; bmu = mu; bzeta = zeta;
2080 bactive = active; bimm = imm; bres = resnorm; bconv = converged;
2081 have_best = true;
2082 }
2083
2084 // The active-set moves, in the order that a failure of each invalidates
2085 // the next: a negative flow means the mode does not fire at all, a
2086 // negative marking means the wrong coordinate was pinned, and only then
2087 // is it worth asking which capacity rows bind.
2088 bool moved = false;
2089 if (nimm > 0) {
2090 double scale = 1.0;
2091 for (double p : phi) scale = std::max(scale, std::fabs(p));
2092 const double thr = -std::max(opt.tol, 1e-10) * scale;
2093 for (std::size_t k = 0; k < nimm && !moved; ++k)
2094 if (phi[k] < thr) {
2095 imm.active[k] = false;
2096 petri_immediate(terms, x, imm);
2097 moved = true;
2098 }
2099 }
2100 if (!moved && nimm > 0) {
2101 const double thr = -std::max(opt.tol, 1e-10);
2102 for (std::size_t s = 0; s < terms.nm && !moved; ++s) {
2103 if (x[s] >= thr) continue;
2104 for (std::size_t k = 0; k < nimm; ++k) {
2105 const PetriMode& md = terms.modes[terms.imm_idx[k]];
2106 if (imm.active[k] &&
2107 std::find(md.arc_slot.begin(), md.arc_slot.end(), s) !=
2108 md.arc_slot.end() &&
2109 imm.bind[k] != static_cast<std::ptrdiff_t>(s)) {
2110 imm.bind[k] = static_cast<std::ptrdiff_t>(s);
2111 petri_immediate(terms, x, imm);
2112 moved = true;
2113 break;
2114 }
2115 }
2116 }
2117 }
2118 if (!moved && ncon > 0) {
2119 std::vector<std::size_t> over;
2120 for (std::size_t c = 0; c < ncon; ++c) {
2121 double val = 0.0;
2122 for (std::size_t s = 0; s < n; ++s) val += con.A(c, s) * x[s];
2123 if (val > con.b[c] + std::max(1e-9, opt.tol) &&
2124 std::find(active.begin(), active.end(), c) == active.end())
2125 over.push_back(c);
2126 }
2127 if (!over.empty()) {
2128 active.insert(active.end(), over.begin(), over.end());
2129 moved = true;
2130 } else {
2131 std::vector<std::size_t> keep;
2132 for (std::size_t i = 0; i < active.size(); ++i)
2133 if (!(i < zeta.size() && zeta[i] > 1 + std::max(1e-9, opt.tol)))
2134 keep.push_back(active[i]);
2135 if (keep.size() != active.size()) {
2136 active = keep;
2137 moved = true;
2138 }
2139 }
2140 }
2141 if (!moved) break;
2142 }
2143
2144 PetriSolution out;
2145 if (have_best && !converged && bconv) {
2146 x = bx; s2 = bs2; phi = bphi; mu = bmu; zeta = bzeta;
2147 active = bactive; imm = bimm; converged = true; resnorm = bres;
2148 }
2149 if (!converged)
2150 out.warnings.push_back(
2151 "The simultaneous closure solve stopped at residual " + std::to_string(resnorm) +
2152 " after " + std::to_string(iters) + " Newton steps without reaching " +
2153 std::to_string(opt.tol) + ". The reported point is the last iterate.");
2154 // A NEGATIVE MARKING IS NOT ROUNDING: the fixed point wanted mass a place
2155 // cannot supply and no immediate mode could be rebound to pin it, so the
2156 // answer is outside the model's own state space and is reported as such.
2157 for (std::size_t s = 0; s < terms.nm; ++s)
2158 if (x[s] < -std::max(opt.tol, 1e-10)) {
2159 out.warnings.push_back(
2160 "The fixed point holds " + std::to_string(x[s]) + " tokens at " +
2161 terms.names_node[terms.coord_node[s]] +
2162 ", which is negative: no immediate transition could be rebound to pin that "
2163 "place at zero. Use SolverCTMC, SolverSSA or SolverLDES for this net.");
2164 break;
2165 }
2166
2167 const solver_detail::Ctx ctx = solver_detail::context(terms, cons, con, imm, active);
2168 std::vector<double> ufin;
2169 ufin.insert(ufin.end(), x.begin(), x.end());
2170 ufin.insert(ufin.end(), s2.begin(), s2.end());
2171 ufin.insert(ufin.end(), phi.begin(), phi.end());
2172 ufin.insert(ufin.end(), mu.begin(), mu.end());
2173 ufin.insert(ufin.end(), zeta.begin(), zeta.end());
2174 const solver_detail::Residual fin = solver_detail::residual(ufin, ctx, false);
2175
2176 // ---- the metrics --------------------------------------------------------
2177 // A PLACE IS AN INF STATION: its queue length is its mean token count and its
2178 // utilization is the same number. Its throughput is the rate at which TOKENS
2179 // leave it, so a consuming mode contributes its firing rate times the arc
2180 // multiplicity -- SolverCTMC's convention, and the one Little's law needs.
2181 out.QN = Matrix<double>(M, K, 0.0);
2182 out.UN = Matrix<double>(M, K, 0.0);
2183 out.RN = Matrix<double>(M, K, 0.0);
2184 out.TN = Matrix<double>(M, K, 0.0);
2185 for (std::size_t s = 0; s < terms.nm; ++s) {
2186 if (terms.coord_station[s] < 0) continue;
2187 const std::size_t i = static_cast<std::size_t>(terms.coord_station[s]);
2188 const std::size_t k = terms.coord_class[s];
2189 out.QN(i, k) += x[s];
2190 out.UN(i, k) = out.QN(i, k);
2191 }
2192 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it =
2193 terms.consumers.begin();
2194 it != terms.consumers.end(); ++it) {
2195 const std::size_t i = it->first / K, k = it->first % K;
2196 const std::vector<double>& w = terms.consumer_w.at(it->first);
2197 double val = 0.0;
2198 for (std::size_t q = 0; q < it->second.size(); ++q) val += w[q] * fin.r[it->second[q]];
2199 out.TN(i, k) += val;
2200 }
2201 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it =
2202 terms.producers.begin();
2203 it != terms.producers.end(); ++it) {
2204 const std::size_t i = it->first / K, k = it->first % K;
2205 double val = 0.0;
2206 for (std::size_t e : it->second) val += fin.r[e];
2207 out.TN(i, k) += val;
2208 }
2209 for (std::size_t i = 0; i < M; ++i)
2210 for (std::size_t k = 0; k < K; ++k)
2211 if (out.TN(i, k) > 1e-14) out.RN(i, k) = out.QN(i, k) / out.TN(i, k);
2212 // A FLUID PLACE reports its level in the first class column: QN is the mean level (the reflected-diffusion
2213 // one under fluid_boundary='diffusion'), UN the fraction of a finite capacity it fills (the level itself when
2214 // unbounded), and TN the rate at which fluid LEAVES it, flows out plus negative impulses, gated at the empty
2215 // level, so that RN = QN/TN is the mean time a unit of fluid spends in the place by Little's law.
2216 std::vector<double> gP, gN;
2217 solver_detail::gains(con, active, zeta, n, gP, gN);
2218 const PetriReport::Fluid fl = solver_detail::fluid_levels(terms, con, imm, active, x, s2, phi, fin.r,
2219 fin.Sigma, opt.fluid_boundary);
2220 for (std::size_t q = 0; q < terms.nf; ++q) {
2221 if (terms.fluid_station[q] < 0 || K == 0) continue;
2222 const std::size_t i = static_cast<std::size_t>(terms.fluid_station[q]);
2223 const std::size_t slot = terms.fluid_slot[q];
2224 // the reported level is clamped to [0, B]: a level pinned at a bound comes out of the solve a rounding
2225 // error past it (-9e-20 under 'reflect'), which the metrics must not show as a negative QLen
2226 const double lev = std::min(std::max(fl.level[q], 0.0), terms.fluid_cap[q]);
2227 out.QN(i, 0) = lev;
2228 out.UN(i, 0) = std::isfinite(terms.fluid_cap[q]) ? lev / terms.fluid_cap[q] : lev;
2229 double outflow = 0.0;
2230 for (std::size_t e = 0; e < terms.nev; ++e) outflow += std::min(terms.D(slot, e), 0.0) * fin.r[e];
2231 out.TN(i, 0) = -gN[slot] * outflow;
2232 out.RN(i, 0) = out.TN(i, 0) > 0.0 ? out.QN(i, 0) / out.TN(i, 0) : 0.0;
2233 }
2234
2235 // ---- the trajectory -----------------------------------------------------
2236 // The immediate flow ALONG the reported path is not the steady-state one: on the seed path it is the
2237 // large-finite-rate approximation the seed itself integrated, so the throughput table and the trajectory it
2238 // is read off describe the same run. A finite horizon replaces the seed path by the transient DAE.
2239 out.t = tseed;
2240 out.xvec_t = xseed;
2241 std::vector<std::vector<double>> phit = solver_detail::seed_flows(terms, xseed, lam);
2242 if (std::isfinite(opt.timespan_end)) {
2243 solver_detail::PetriTransient tr =
2244 solver_detail::petri_transient(terms, cons, con, imm, active, opt, phi, zeta, out.warnings);
2245 out.t = tr.t;
2246 out.xvec_t = tr.xt;
2247 phit = tr.phit;
2248 out.Sigmat = tr.Sigmat;
2249 out.tvar = tr.tvar;
2250 out.switches = tr.switches;
2251 }
2252 solver_detail::metrics_t(terms, out.xvec_t, s2, phit, M, K, out);
2253 out.x = x;
2254 out.iters = iters;
2255 out.resnorm = resnorm;
2256 out.converged = converged;
2257 out.Sigma = fin.Sigma;
2258 out.QVar = Matrix<double>(M, K, 0.0);
2259 out.QStd = Matrix<double>(M, K, 0.0);
2260 for (std::size_t s = 0; s < terms.nm; ++s) {
2261 if (terms.coord_station[s] < 0) continue;
2262 const std::size_t i = static_cast<std::size_t>(terms.coord_station[s]);
2263 out.QVar(i, terms.coord_class[s]) = std::max(0.0, fin.Sigma(s, s));
2264 }
2265 for (std::size_t q = 0; q < terms.nf; ++q)
2266 if (terms.fluid_station[q] >= 0 && K > 0)
2267 out.QVar(static_cast<std::size_t>(terms.fluid_station[q]), 0) = fl.level_var[q];
2268 for (std::size_t i = 0; i < M; ++i)
2269 for (std::size_t k = 0; k < K; ++k) out.QStd(i, k) = std::sqrt(out.QVar(i, k));
2270
2271 // ---- the report ---------------------------------------------------------
2272 out.petri.marking = Matrix<double>(terms.I, terms.K, 0.0);
2273 out.petri.marking_var = Matrix<double>(terms.I, terms.K, 0.0);
2274 for (std::size_t s = 0; s < terms.nm; ++s) {
2275 out.petri.marking(terms.coord_node[s], terms.coord_class[s]) = x[s];
2276 out.petri.marking_var(terms.coord_node[s], terms.coord_class[s]) =
2277 std::max(0.0, fin.Sigma(s, s));
2278 }
2279 for (const PetriMode& md : terms.modes) out.petri.mode_label.push_back(md.label);
2280 out.petri.mode_flow.assign(terms.modes.size(), 0.0);
2281 for (std::size_t j = 0; j < terms.modes.size(); ++j) {
2282 double val = 0.0;
2283 for (std::size_t e = 0; e < terms.nev; ++e)
2284 if (terms.ev_mode[e] == static_cast<int>(j) &&
2285 (terms.ev_kind[e] == 1 || terms.ev_kind[e] == 4))
2286 val += fin.r[e];
2287 out.petri.mode_flow[j] = val;
2288 }
2289 out.petri.immediate_flow = phi;
2290 out.petri.invariant_label = cons.label;
2291 out.petri.invariant_value = cons.N;
2292 out.petri.invariant_error.assign(cons.C.rows(), 0.0);
2293 for (std::size_t c = 0; c < cons.C.rows(); ++c) {
2294 double val = 0.0;
2295 for (std::size_t s = 0; s < terms.nstate; ++s) val += cons.C(c, s) * x[s];
2296 out.petri.invariant_error[c] = val - cons.N[c];
2297 }
2298 out.petri.capacity_label = con.label;
2299 out.petri.capacity_active = active;
2300 out.petri.capacity_fraction = zeta;
2301 out.petri.pinned = imm.pins;
2302 out.petri.Sigma = fin.Sigma;
2303 out.petri.fluid = fl;
2304 return out;
2305}
2306
2307} // namespace petri
2308} // namespace fluid
2309} // namespace line
2310
2311#endif // LINE_SOLVERS_FLUID_PETRI_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
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The second-order fluid methods: fluid_moment_terms.m, fluid_lyapunov.m, fluid_drift_jacobian....
The closures, the closed enabling term, the rate vector and the drift Jacobian of a stochastic Petri ...
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.
SnFluidVerdict sn_fluid_refusal(const qn::NetworkStruct< T > &sn)
Whether the net breaks a structural rule of an FSPN.
void petri_immediate(const PetriTerms &t, const std::vector< double > &x, PetriImmediate &imm)
An immediate transition has no rate: its fluid limit is a FLOW, an algebraic unknown pinned by the co...
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.
PetriConstraints petri_constraints(const qn::NetworkStruct< T > &sn, const PetriTerms &t)
THE GATE IS A LOSS ON THE DEPOSIT: LINE loses the tokens a firing would push past a place's capacity,...
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.
PetriVerdict petri_applicable(const qn::NetworkStruct< T > &sn)
A QUEUEING STATION IS THE ONE STRUCTURAL EXCLUSION: a net whose tokens also visit a Queue or a Delay ...
double petri_coarse_tol()
GlobalConstants.CoarseTol, also the relative width of the seed's smoothed inhibitor gate.
Matrix< double > petri_jacobian(const PetriTerms &t, const PetriTheta &th)
Drift Jacobian A = D * dR/dX.
PetriTerms petri_build_terms(const qn::NetworkStruct< T > &sn)
Assemble the drift terms of a net.
PetriConservation petri_conservation(const PetriTerms &t)
The conserved quantities, as equations: u'D = 0 implies u'x is constant.
double petri_fine_tol()
GlobalConstants.FineTol, the reference's own "effectively zero".
PetriSolution solver_fluid_petri(const qn::NetworkStruct< T > &sn, const PetriOptions &opt=PetriOptions())
Fluid analysis of a stochastic Petri net.
Matrix< double > fluid_lyapunov(const Matrix< double > &A, const Matrix< double > &Qdiff, const Matrix< double > &D, FluidLyapunovInfo &info, double tol=-1.0)
Port of fluid_lyapunov.m: the stationary covariance of the linear noise approximation.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:348
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
bool nz(const Val &v)
@ Rm
repairman: rates from the balanced job bound, column-max reading
Definition pfqn_mci.h:94
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< double > pinv(const Matrix< double > &A)
Moore-Penrose pseudo-inverse, A^+ = V diag(1/s_i) U^T over the singular values above max(m,...
Definition svd.h:93
OdeSolution< T > ode_rosenbrock4(const F &f, const J &jac, const T &t0, const T &t1, const std::vector< T > &y0, const OdeOptions< T > &opt)
Integrate y' = f(t,y) from t0 to t1 with an analytic Jacobian.
Definition ode.h:303
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
Adaptive stiff ODE integrator: a four-stage Rosenbrock method of order four with an embedded order-th...
Port of matlab/src/api/sn/sn_fluid_refusal.m.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Integration controls.
Definition ode.h:117
bool store_trajectory
keep every accepted point, not just the last
Definition ode.h:124
T atol
absolute tolerance per component
Definition ode.h:119
std::size_t max_steps
abort after this many accepted steps
Definition ode.h:123
T rtol
relative tolerance per component
Definition ode.h:118
Result of an integration.
Definition ode.h:142
std::vector< std::vector< T > > y
y[i] is the state at t[i]
Definition ode.h:144
std::vector< T > t
accepted time points, t[0] = t0
Definition ode.h:143
The verdict of sn_fluid_refusal: ok when no rule is broken, else the reason.
The conserved quantities of a net, as equations.
Definition fluid_petri.h:65
std::vector< std::string > label
Definition fluid_petri.h:69
Every finite place capacity as a linear row A x <= b, and the two bounds of every fluid place.
std::vector< std::string > label
std::vector< std::vector< bool > > cover
The active set of the immediate modes and the equations pinning their flows.
std::vector< std::ptrdiff_t > bind
std::vector< std::size_t > pins
One firing mode of one transition, with its arcs and its firing process.
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 > inh_thr
Tuning of the outer solve.
std::size_t dae_maxcov
options.config.dae_maxcov: the largest covariance block integrated along the trajectory.
std::vector< double > tranpoints
options.tranpoints: instants added to the integrator's own steps, read off its dense output.
double timespan_start
options.timespan: a finite end integrates the transient DAE over [start, end] (MATLAB local_transient...
std::string fluid_boundary
options.config.fluid_boundary: how a fluid level held at a bound is reported, reflect (the bound itse...
MATLAB moments.petri.fluid: per fluid place its name, reported level and level variance,...
std::vector< std::string > names
What the Petri route computes that the station table has no column for.
std::vector< std::size_t > pinned
std::vector< double > capacity_fraction
std::vector< std::string > mode_label
std::vector< double > mode_flow
std::vector< double > invariant_error
struct line::fluid::petri::PetriReport::Fluid fluid
std::vector< std::string > invariant_label
std::vector< double > invariant_value
std::vector< std::string > capacity_label
std::vector< std::size_t > capacity_active
std::vector< double > immediate_flow
Everything solver_fluid_petri returns.
std::vector< Matrix< double > > Sigmat
The transient marking covariance at each tvar instant, over the cov_idx coordinates (row-major nc*nc)...
std::vector< std::vector< std::vector< double > > > QNt
The trajectory tables, indexed [station][class][time point]; filled along t.
std::vector< std::string > warnings
std::vector< std::vector< double > > xvec_t
std::vector< std::vector< std::vector< double > > > UNt
std::vector< std::pair< double, int > > switches
The active-set switches of the transient: (time, event index), MATLAB's switches.
std::vector< std::vector< std::vector< double > > > TNt
The assembled drift terms of a net.
std::vector< std::size_t > places
std::size_t nstate
marking coordinates plus phase coordinates plus fluid levels
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 > imm_idx
std::vector< std::string > names_node
0-based, one per node
std::vector< std::string > fluid_name
std::vector< std::size_t > fluid_slot
std::vector< PetriMode > modes
Matrix< double > D
(nstate x nev) incidence
Whether the fluid Petri route can answer this model, and why not.
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
const std::vector< FluidGuard > & fluid_guards(std::size_t m) const
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...