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 <cmath>
38#include <cstddef>
39#include <limits>
40#include <map>
41#include <set>
42#include <string>
43#include <vector>
44
48#include "line/util/error.h"
49#include "line/util/linalg.h"
50#include "line/util/matrix.h"
51#include "line/util/ode.h"
52#include "line/util/svd.h"
53
54namespace line {
55namespace fluid {
56namespace petri {
57
58// ============================ conservation ===============================
59
60/** The conserved quantities of a net, as equations. */
63 std::vector<double> N;
64 double leak = 0.0;
65 std::vector<std::string> label;
66};
67
68namespace cons_detail {
69
70/**
71 * A rational basis of the null space, as MATLAB's `null(A,'r')` returns.
72 *
73 * D is INTEGRAL -- arc multiplicities and unit phase moves -- so the reduced row
74 * echelon form gives exact rational rows, which keeps each conservation row
75 * readable as a statement about named places instead of an arbitrary orthogonal
76 * mixture. An SVD basis would span the same space and say nothing.
77 */
78inline Matrix<double> null_rational(const Matrix<double>& A) {
79 const std::size_t rows = A.rows(), cols = A.cols();
80 if (rows == 0 || cols == 0) return Matrix<double>(cols, 0, 0.0);
81 Matrix<double> R = A;
82 std::vector<std::size_t> piv;
83 std::size_t r = 0;
84 const double tol = 1e-12;
85 for (std::size_t c = 0; c < cols && r < rows; ++c) {
86 std::size_t k = r;
87 double best = std::fabs(R(r, c));
88 for (std::size_t i = r + 1; i < rows; ++i)
89 if (std::fabs(R(i, c)) > best) {
90 best = std::fabs(R(i, c));
91 k = i;
92 }
93 if (best <= tol) {
94 for (std::size_t i = r; i < rows; ++i) R(i, c) = 0.0;
95 continue;
96 }
97 if (k != r)
98 for (std::size_t j = 0; j < cols; ++j) std::swap(R(r, j), R(k, j));
99 const double p = R(r, c);
100 for (std::size_t j = 0; j < cols; ++j) R(r, j) /= p;
101 for (std::size_t i = 0; i < rows; ++i) {
102 if (i == r) continue;
103 const double f = R(i, c);
104 if (f == 0.0) continue;
105 for (std::size_t j = 0; j < cols; ++j) R(i, j) -= f * R(r, j);
106 }
107 piv.push_back(c);
108 ++r;
109 }
110 std::vector<std::size_t> free;
111 for (std::size_t c = 0; c < cols; ++c)
112 if (std::find(piv.begin(), piv.end(), c) == piv.end()) free.push_back(c);
113 Matrix<double> Z(cols, free.size(), 0.0);
114 for (std::size_t i = 0; i < free.size(); ++i) {
115 Z(free[i], i) = 1.0;
116 for (std::size_t rr = 0; rr < piv.size(); ++rr) Z(piv[rr], i) = -R(rr, free[i]);
117 }
118 return Z;
119}
120
121/** %g formatting, so an integral weight prints without a decimal tail. */
122inline std::string trim(double v) {
123 if (v == std::rint(v) && std::fabs(v) < 1e15)
124 return std::to_string(static_cast<long long>(v));
125 char buf[32];
126 std::snprintf(buf, sizeof(buf), "%g", v);
127 return std::string(buf);
128}
129
130/** One conservation row, written out over the coordinates it touches. */
131inline std::string row_label(const PetriTerms& t, const Matrix<double>& C, std::size_t r) {
132 std::string out;
133 for (std::size_t s = 0; s < C.cols(); ++s) {
134 const double w = C(r, s);
135 if (w == 0.0) continue;
136 std::string nm;
137 if (s < t.nm) {
138 nm = t.names_node[t.coord_node[s]] + "(class " +
139 std::to_string(t.coord_class[s] + 1) + ")";
140 } else {
141 nm = "phase";
142 for (std::size_t j = 0; j < t.modes.size(); ++j)
143 for (std::size_t q = 0; q < t.modes[j].zblk.size(); ++q)
144 if (t.modes[j].zblk[q] == s)
145 nm = t.modes[j].label + " phase " + std::to_string(q + 1);
146 }
147 if (!out.empty()) out += " + ";
148 out += (w == 1.0) ? nm : (trim(w) + "*" + nm);
149 }
150 return out;
151}
152
153} // namespace cons_detail
154
155/**
156 * The conserved quantities, as equations: `u'D = 0` implies `u'x` is constant.
157 *
158 * On the marking coordinates those u are the net's P-invariants; on a mode's
159 * phase block the all-ones vector is one of them, which is the statement that
160 * the phase coordinates are a distribution. Both come out of the SAME null
161 * space, so the phase normalisation needs no separate row.
162 *
163 * AN OPEN NET LOSES THE ROWS ITS ARRIVALS BREAK, automatically: the arrival
164 * columns are part of D, so a u an arrival moves is not in the null space.
165 */
168 if (t.D.rows() == 0 || t.D.cols() == 0 || t.nev == 0) {
169 cons.C = Matrix<double>(0, t.nstate, 0.0);
170 return cons;
171 }
172 Matrix<double> Dt(t.D.cols(), t.D.rows(), 0.0);
173 for (std::size_t i = 0; i < t.D.rows(); ++i)
174 for (std::size_t j = 0; j < t.D.cols(); ++j) Dt(j, i) = t.D(i, j);
175 const Matrix<double> Z = cons_detail::null_rational(Dt);
176 Matrix<double> C(Z.cols(), Z.rows(), 0.0);
177 for (std::size_t i = 0; i < Z.rows(); ++i)
178 for (std::size_t j = 0; j < Z.cols(); ++j) C(j, i) = Z(i, j);
179 // A rational basis has exact zeros; anything below this is a rounding
180 // artefact of the elimination, not a coefficient.
181 for (std::size_t i = 0; i < C.rows(); ++i)
182 for (std::size_t j = 0; j < C.cols(); ++j)
183 if (std::fabs(C(i, j)) < 1e-12) C(i, j) = 0.0;
184 // Scale each row by its smallest nonzero magnitude, so a row reads as a
185 // statement about named places with small integer weights.
186 std::vector<std::size_t> keep;
187 for (std::size_t i = 0; i < C.rows(); ++i) {
188 double smallest = std::numeric_limits<double>::infinity();
189 for (std::size_t j = 0; j < C.cols(); ++j)
190 if (C(i, j) != 0.0) smallest = std::min(smallest, std::fabs(C(i, j)));
191 if (!std::isfinite(smallest)) continue;
192 for (std::size_t j = 0; j < C.cols(); ++j) C(i, j) /= smallest;
193 keep.push_back(i);
194 }
195 cons.C = Matrix<double>(keep.size(), t.nstate, 0.0);
196 for (std::size_t r = 0; r < keep.size(); ++r)
197 for (std::size_t j = 0; j < t.nstate; ++j) cons.C(r, j) = C(keep[r], j);
198 cons.N.assign(cons.C.rows(), 0.0);
199 for (std::size_t r = 0; r < cons.C.rows(); ++r) {
200 double v = 0.0;
201 for (std::size_t j = 0; j < t.nstate; ++j) v += cons.C(r, j) * t.x0[j];
202 cons.N[r] = v;
203 }
204 for (std::size_t r = 0; r < cons.C.rows(); ++r)
205 for (std::size_t c = 0; c < t.nev; ++c) {
206 double v = 0.0;
207 for (std::size_t j = 0; j < t.nstate; ++j) v += cons.C(r, j) * t.D(j, c);
208 cons.leak = std::max(cons.leak, std::fabs(v));
209 }
210 for (std::size_t r = 0; r < cons.C.rows(); ++r)
211 cons.label.push_back(cons_detail::row_label(t, cons.C, r));
212 return cons;
213}
214
215// ============================ capacities =================================
216
217/** Every finite place capacity as a linear row `A x <= b`. */
220 std::vector<double> b;
221 std::vector<std::string> label;
222 std::vector<std::vector<bool>> cover;
223};
224
225/**
226 * THE GATE IS A LOSS ON THE DEPOSIT: LINE loses the tokens a firing would push
227 * past a place's capacity, so the fluid analogue scales the DEPOSIT leg of every
228 * event adding mass to the capped place and leaves the removal leg alone. The
229 * rows here name which coordinates are capped; the scaling lives in the tangent
230 * clamp.
231 */
232template <class T>
234 std::vector<std::vector<double>> rows;
235 std::vector<double> bs;
236 std::vector<std::string> labels;
237 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
238 const std::size_t ind = t.places[pi];
239 const std::size_t ist = sn.nodes[ind - 1].station;
240 std::vector<std::size_t> slots;
241 for (std::size_t k = 0; k < t.K; ++k)
242 if (t.pidx[ind - 1][k] >= 0)
243 slots.push_back(static_cast<std::size_t>(t.pidx[ind - 1][k]));
244 if (slots.empty()) continue;
245 if (ist >= 1 && ist <= sn.stations.size()) {
246 const double cap = sn.stations[ist - 1].cap;
247 if (std::isfinite(cap)) {
248 std::vector<double> row(t.nstate, 0.0);
249 for (std::size_t s : slots) row[s] = 1.0;
250 rows.push_back(row);
251 bs.push_back(cap);
252 labels.push_back("capacity " + cons_detail::trim(cap) + " of place " +
253 t.names_node[ind - 1]);
254 }
255 for (std::size_t k = 0; k < t.K; ++k) {
256 if (t.pidx[ind - 1][k] < 0) continue;
257 if (k >= sn.stations[ist - 1].classcap.size()) continue;
258 const double ccap = sn.stations[ist - 1].classcap[k];
259 if (!std::isfinite(ccap)) continue;
260 std::vector<double> row(t.nstate, 0.0);
261 row[static_cast<std::size_t>(t.pidx[ind - 1][k])] = 1.0;
262 rows.push_back(row);
263 bs.push_back(ccap);
264 labels.push_back("class-" + std::to_string(k + 1) + " capacity " +
265 cons_detail::trim(ccap) + " of place " + t.names_node[ind - 1]);
266 }
267 }
268 }
269 // TWO ROWS THAT SAY THE SAME THING ARE A SINGULAR NEWTON SYSTEM, not a
270 // redundancy the least squares absorbs, so an exact duplicate is pruned and
271 // the tighter bound survives.
272 std::vector<bool> keep(rows.size(), true);
273 for (std::size_t c = 0; c < rows.size(); ++c) {
274 if (!keep[c]) continue;
275 for (std::size_t d = c + 1; d < rows.size(); ++d) {
276 if (!keep[d] || rows[c] != rows[d]) continue;
277 if (bs[d] < bs[c]) {
278 keep[c] = false;
279 break;
280 }
281 keep[d] = false;
282 }
283 }
284 std::vector<std::size_t> idx;
285 for (std::size_t i = 0; i < keep.size(); ++i)
286 if (keep[i]) idx.push_back(i);
288 con.A = Matrix<double>(idx.size(), t.nstate, 0.0);
289 con.b.assign(idx.size(), 0.0);
290 con.cover.assign(idx.size(), std::vector<bool>(t.nstate, false));
291 for (std::size_t r = 0; r < idx.size(); ++r) {
292 for (std::size_t s = 0; s < t.nstate; ++s) {
293 con.A(r, s) = rows[idx[r]][s];
294 con.cover[r][s] = rows[idx[r]][s] > 0.0;
295 }
296 con.b[r] = bs[idx[r]];
297 con.label.push_back(labels[idx[r]]);
298 }
299 return con;
300}
301
302// ============================ the immediates =============================
303
304/** The active set of the immediate modes and the equations pinning their flows. */
306 enum Kind { PIN = 0, RATIO = 1, ZERO = 2 };
307 struct Row {
309 std::size_t a = 0, b = 0;
310 double wa = 0.0, wb = 0.0;
311 };
312 std::size_t n = 0;
313 std::vector<bool> active;
314 std::vector<std::ptrdiff_t> bind;
315 std::vector<std::size_t> pins;
316 std::vector<Row> rows;
317 bool initialized = false;
318};
319
320namespace imm_detail {
321
322/**
323 * True when an inhibitor arc of this mode has reached its threshold.
324 *
325 * A HARD TEST ON THE MEAN, AND A KNOWN WRONG ANSWER WHEN THE MEAN SITS ON THE
326 * THRESHOLD -- a timed mode closes the same indicator smoothly, as
327 * `Phi((thr-m)/sd)` in `petri_theta`; this path does not. See
328 * `_kb/06-solver-catalog.md` for the measurement and for what a real fix costs.
329 */
330inline bool inhibited(const PetriMode& md, const std::vector<double>& x) {
331 for (std::size_t b = 0; b < md.inh_slot.size(); ++b)
332 if (x[md.inh_slot[b]] >= md.inh_thr[b]) return true;
333 return false;
334}
335
336} // namespace imm_detail
337
338/**
339 * An immediate transition has no rate: its fluid limit is a FLOW, an algebraic
340 * unknown pinned by the constraint that its binding input place holds no mass,
341 *
342 * phi_j >= 0, x_b = 0 for the coordinate b that binds mode j
343 *
344 * with the GSPN conflict rule supplying the extra equation when two modes drain
345 * one place: `phi_j*weight_l = phi_l*weight_j` among the enabled modes of highest
346 * firing priority, and `phi = 0` below it.
347 *
348 * THE COUNT IS SQUARE BY CONSTRUCTION: V pins plus (F-V) ratio rows is F
349 * equations for F flows.
350 */
351inline void petri_immediate(const PetriTerms& t, const std::vector<double>& x,
352 PetriImmediate& imm) {
353 const std::size_t n = t.imm_idx.size();
354 if (!imm.initialized) {
355 imm.n = n;
356 imm.active.assign(n, true);
357 imm.bind.assign(n, -1);
358 imm.pins.clear();
359 // An inhibited mode never fires, so it neither carries a flow nor empties
360 // a place. AN EMPTY INPUT PLACE IS NOT A REASON TO DEACTIVATE -- that is
361 // the normal state of an enabled immediate mode, and the whole content of
362 // its pin.
363 for (std::size_t k = 0; k < n; ++k) {
364 const PetriMode& md = t.modes[t.imm_idx[k]];
365 if (md.arc_slot.empty())
366 throw InputError("fluid_petri_immediate: immediate mode " + md.label +
367 " has no enabling arc, so nothing bounds its firing flow and "
368 "the net has no fluid limit. Give it an input place, or make "
369 "it timed");
370 if (imm_detail::inhibited(md, x)) imm.active[k] = false;
371 }
372 imm.initialized = true;
373 }
374
375 // ---- the assignment: each active mode binds the input arc it is shortest of
376 for (std::size_t k = 0; k < n; ++k) {
377 if (!imm.active[k]) {
378 imm.bind[k] = -1;
379 continue;
380 }
381 const PetriMode& md = t.modes[t.imm_idx[k]];
382 if (imm.bind[k] >= 0 &&
383 std::find(md.arc_slot.begin(), md.arc_slot.end(),
384 static_cast<std::size_t>(imm.bind[k])) != md.arc_slot.end())
385 continue; // a binding the caller set explicitly is kept
386 std::size_t best = md.arc_slot[0];
387 double best_lev = x[md.arc_slot[0]] / md.arc_w[0];
388 for (std::size_t a = 1; a < md.arc_slot.size(); ++a) {
389 const double lev = x[md.arc_slot[a]] / md.arc_w[a];
390 if (lev < best_lev) {
391 best_lev = lev;
392 best = md.arc_slot[a];
393 }
394 }
395 imm.bind[k] = static_cast<std::ptrdiff_t>(best);
396 }
397
398 // ---- the equations
399 std::set<std::size_t> pinset;
400 for (std::size_t k = 0; k < n; ++k)
401 if (imm.active[k] && imm.bind[k] >= 0)
402 pinset.insert(static_cast<std::size_t>(imm.bind[k]));
403 imm.pins.assign(pinset.begin(), pinset.end());
404 imm.rows.clear();
405 for (std::size_t p : imm.pins) {
407 row.kind = PetriImmediate::PIN;
408 row.a = p;
409 imm.rows.push_back(row);
410 std::vector<std::size_t> grp;
411 for (std::size_t k = 0; k < n; ++k)
412 if (imm.active[k] && imm.bind[k] == static_cast<std::ptrdiff_t>(p)) grp.push_back(k);
413 if (grp.size() <= 1) continue;
414 int topprio = std::numeric_limits<int>::min();
415 for (std::size_t k : grp) topprio = std::max(topprio, t.modes[t.imm_idx[k]].prio);
416 std::vector<std::size_t> top, low;
417 for (std::size_t k : grp)
418 (t.modes[t.imm_idx[k]].prio == topprio ? top : low).push_back(k);
419 const double w0 = t.modes[t.imm_idx[top[0]]].weight;
420 for (std::size_t i = 1; i < top.size(); ++i) {
423 rr.a = top[0];
424 rr.b = top[i];
425 rr.wa = w0;
426 rr.wb = t.modes[t.imm_idx[top[i]]].weight;
427 imm.rows.push_back(rr);
428 }
429 for (std::size_t k : low) {
432 rr.a = k;
433 imm.rows.push_back(rr);
434 }
435 }
436 for (std::size_t k = 0; k < n; ++k)
437 if (!imm.active[k]) {
440 rr.a = k;
441 imm.rows.push_back(rr);
442 }
443}
444
445// ============================ the refusals ===============================
446
447/** Whether the fluid Petri route can answer this model, and why not. */
449 bool ok = true;
450 std::string reason;
451};
452
453/**
454 * A QUEUEING STATION IS THE ONE STRUCTURAL EXCLUSION: a net whose tokens also
455 * visit a Queue or a Delay is two formalisms at once, and LINE has no reference
456 * semantics for the hand-off.
457 */
458template <class T>
460 PetriVerdict v;
461 const std::size_t I = sn.nodes.size();
462 bool has_transition = false;
463 for (std::size_t i = 0; i < I; ++i)
464 if (sn.nodes[i].nodetype == lang::NodeType::Transition) has_transition = true;
465 if (!has_transition) {
466 v.ok = false;
467 v.reason = "the model has no Transition node, so it is not a Petri net";
468 return v;
469 }
470 for (std::size_t i = 0; i < I; ++i) {
471 const lang::NodeType nt = sn.nodes[i].nodetype;
474 v.ok = false;
475 v.reason = "node " + sn.nodes[i].name + " is a " +
476 std::string(lang::node_type_to_text(nt)) +
477 ". The fluid Petri route solves the marking of a Petri net, and a model "
478 "that also holds queueing stations is two formalisms at once with no "
479 "reference semantics for the hand-off; use SolverCTMC, SolverJMT, "
480 "SolverSSA or SolverLDES";
481 return v;
482 }
483 }
484 // A queueing place declares a service process, which is what turns it into a
485 // station with an embedded queue and a depository.
486 for (std::size_t i = 0; i < I; ++i) {
487 if (sn.nodes[i].nodetype != lang::NodeType::Place) continue;
488 const std::size_t ist = sn.nodes[i].station;
489 if (ist < 1 || ist > sn.nstations) continue;
490 for (std::size_t k = 0; k < sn.nclasses; ++k) {
491 const double rate = num_traits<T>::to_double(sn.rates(ist - 1, k));
492 if (!std::isnan(rate) && rate > 0) {
493 v.ok = false;
494 v.reason = "place " + sn.nodes[i].name +
495 " is a QUEUEING place (it declares a service process), whose embedded "
496 "queue this drift does not carry; use SolverLDES";
497 return v;
498 }
499 }
500 }
501 return v;
502}
503
504// ============================ the driver =================================
505
506/** What the Petri route computes that the station table has no column for. */
509 std::vector<std::string> mode_label;
510 std::vector<double> mode_flow, immediate_flow;
511 std::vector<std::string> invariant_label;
512 std::vector<double> invariant_value, invariant_error;
513 std::vector<std::string> capacity_label;
514 std::vector<std::size_t> capacity_active;
515 std::vector<double> capacity_fraction;
516 std::vector<std::size_t> pinned;
518};
519
520/** Everything `solver_fluid_petri` returns. */
523 std::vector<double> t;
524 std::vector<std::vector<double>> xvec_t;
525 std::vector<std::vector<std::vector<double>>> QNt, UNt, TNt;
526 std::vector<double> x;
527 std::size_t iters = 0;
528 double resnorm = std::numeric_limits<double>::infinity();
529 bool converged = false;
530 double runtime = 0.0;
533 std::vector<std::string> warnings;
534};
535
536/** Tuning of the outer solve. */
538 std::size_t dae_maxstate = 100;
539 double tol = 1e-8;
540 std::size_t newton_max = 50;
541};
542
543namespace solver_detail {
544
545/** Everything the residual needs that does not change within one active set. */
546struct Ctx {
547 const PetriTerms* terms = nullptr;
548 const PetriImmediate* imm = nullptr;
549 std::vector<std::size_t> active;
550 const PetriConstraints* con = nullptr;
552 std::vector<double> N;
553 Matrix<double> Dp, Dn;
554};
555
556/**
557 * THE CONSERVATION ROWS A BINDING CAP BREAKS ARE DROPPED: a capped place loses
558 * the tokens that do not fit, so a conserved quantity supported on it is not
559 * conserved while the cap binds, and keeping its row would state an equation the
560 * drift contradicts -- a singular Newton system rather than an inaccuracy.
561 */
562inline Ctx context(const PetriTerms& terms, const PetriConservation& cons,
563 const PetriConstraints& con, const PetriImmediate& imm,
564 const std::vector<std::size_t>& active) {
565 Ctx ctx;
566 ctx.terms = &terms;
567 ctx.imm = &imm;
568 ctx.active = active;
569 ctx.con = &con;
570 Matrix<double> C = cons.C;
571 std::vector<double> N = cons.N;
572 if (!active.empty() && C.rows() > 0) {
573 std::vector<bool> hit(terms.nstate, false);
574 for (std::size_t c : active)
575 for (std::size_t s = 0; s < terms.nstate; ++s)
576 if (con.cover[c][s]) hit[s] = true;
577 std::vector<std::size_t> keep;
578 for (std::size_t r = 0; r < C.rows(); ++r) {
579 bool drop = false;
580 for (std::size_t s = 0; s < terms.nstate && !drop; ++s)
581 if (hit[s] && C(r, s) != 0.0) drop = true;
582 if (!drop) keep.push_back(r);
583 }
584 Matrix<double> Ck(keep.size(), terms.nstate, 0.0);
585 std::vector<double> Nk(keep.size(), 0.0);
586 for (std::size_t i = 0; i < keep.size(); ++i) {
587 for (std::size_t s = 0; s < terms.nstate; ++s) Ck(i, s) = C(keep[i], s);
588 Nk[i] = N[keep[i]];
589 }
590 C = Ck;
591 N = Nk;
592 }
593 ctx.C = C;
594 ctx.N = N;
595 ctx.Dp = Matrix<double>(terms.D.rows(), terms.D.cols(), 0.0);
596 ctx.Dn = Matrix<double>(terms.D.rows(), terms.D.cols(), 0.0);
597 for (std::size_t i = 0; i < terms.D.rows(); ++i)
598 for (std::size_t j = 0; j < terms.D.cols(); ++j) {
599 ctx.Dp(i, j) = std::max(terms.D(i, j), 0.0);
600 ctx.Dn(i, j) = std::min(terms.D(i, j), 0.0);
601 }
602 return ctx;
603}
604
605/** The unknown vector, split into its blocks. */
606struct Unpacked {
607 std::vector<double> x, s2, phi, mu, zeta;
608};
609
610inline Unpacked unpack(const std::vector<double>& u, const PetriTerms& t,
611 const PetriImmediate& imm, std::size_t na) {
612 const std::size_t n = t.nstate, npair = t.npair, ni = imm.n, nl = t.latch_mode.size();
613 Unpacked up;
614 up.x.assign(u.begin(), u.begin() + n);
615 up.s2.assign(u.begin() + n, u.begin() + n + npair);
616 up.phi.assign(u.begin() + n + npair, u.begin() + n + npair + ni);
617 for (std::size_t i = 0; i < up.phi.size(); ++i) up.phi[i] = std::max(0.0, up.phi[i]);
618 up.mu.assign(u.begin() + n + npair + ni, u.begin() + n + npair + ni + nl);
619 up.zeta.assign(u.begin() + n + npair + ni + nl, u.begin() + n + npair + ni + nl + na);
620 for (std::size_t i = 0; i < up.zeta.size(); ++i) up.zeta[i] = std::max(0.0, up.zeta[i]);
621 return up;
622}
623
624inline std::ptrdiff_t index_of(const std::vector<std::size_t>& a, std::size_t v) {
625 for (std::size_t i = 0; i < a.size(); ++i)
626 if (a[i] == v) return static_cast<std::ptrdiff_t>(i);
627 return -1;
628}
629
630/**
631 * The reduction of the fluctuation onto the manifold the fast and clamped
632 * directions leave free.
633 *
634 * AN IMMEDIATE PIN REDUCES OBLIQUELY, ALONG THE FAST REACTION ITSELF, and this
635 * is the one place where the orthogonal projector the queueing twin uses is
636 * WRONG rather than merely different: a slow event depositing into a pinned
637 * place is answered instantly by the immediate transition, so its effective jump
638 * is its own plus the immediate flow it triggers -- the token is forwarded, not
639 * lost. An orthogonal projection deletes the deposit and destroys mass in the
640 * diffusion.
641 *
642 * P = I - Cf * G * E_B, G = G0 * (E_B Cf G0)^-1
643 *
644 * A CAPACITY CAP REDUCES ORTHOGONALLY -- mass that does not fit is genuinely
645 * lost, so there is nothing to forward it to. A SERVER LATCH REDUCES
646 * ORTHOGONALLY TOO, on the LINEARISED row `[-dtheta_j/dm, 1 over the phase
647 * block]`, which is why this projector depends on the iterate.
648 *
649 * THE PSEUDO-INVERSE IS THE RANK-REVEALING ONE, not a regularised normal-equation
650 * solve: the oblique reduction must ANNIHILATE the pinned coordinate exactly, and
651 * a Tikhonov term of 1e-12 leaves it at 1e-12, after which the reduced generator
652 * keeps a marginal eigenvalue and `fluid_lyapunov` refuses the fixed point as
653 * non-hyperbolic -- a failure to reduce reported as a property of the model.
654 */
655inline bool clamp_tangent(const PetriTerms& terms, const PetriImmediate& imm,
656 const PetriConstraints& con, const std::vector<std::size_t>& active,
657 const PetriTheta* th, Matrix<double>& T) {
658 const std::vector<std::size_t>& idx = terms.cov_idx;
659 const std::size_t nc = idx.size();
660 T = Matrix<double>(nc, nc, 0.0);
661 for (std::size_t i = 0; i < nc; ++i) T(i, i) = 1.0;
662
663 std::vector<std::size_t> actk;
664 for (std::size_t k = 0; k < imm.n; ++k)
665 if (imm.active[k] && imm.bind[k] >= 0) actk.push_back(k);
666 const std::vector<std::size_t>& B = imm.pins;
667 if (!actk.empty() && !B.empty()) {
668 Matrix<double> Cf(nc, actk.size(), 0.0);
669 for (std::size_t a = 0; a < actk.size(); ++a) {
670 const std::vector<double>& cv = terms.modes[terms.imm_idx[actk[a]]].cvec;
671 for (std::size_t i = 0; i < nc; ++i) Cf(i, a) = cv[idx[i]];
672 }
673 Matrix<double> G0(actk.size(), B.size(), 0.0);
674 for (std::size_t jb = 0; jb < B.size(); ++jb) {
675 std::vector<std::size_t> grp;
676 double wsum = 0.0;
677 for (std::size_t q = 0; q < actk.size(); ++q)
678 if (imm.bind[actk[q]] == static_cast<std::ptrdiff_t>(B[jb])) {
679 grp.push_back(q);
680 wsum += terms.modes[terms.imm_idx[actk[q]]].weight;
681 }
682 if (grp.empty()) continue;
683 const bool uniform = !(wsum > 0);
684 for (std::size_t q : grp) {
685 const double w =
686 uniform ? 1.0 : terms.modes[terms.imm_idx[actk[q]]].weight;
687 G0(q, jb) = w / (uniform ? static_cast<double>(grp.size()) : wsum);
688 }
689 }
690 Matrix<double> EB(B.size(), nc, 0.0);
691 for (std::size_t jb = 0; jb < B.size(); ++jb) {
692 const std::ptrdiff_t at = index_of(idx, B[jb]);
693 if (at >= 0) EB(jb, static_cast<std::size_t>(at)) = 1.0;
694 }
695 const Matrix<double> Mb = matmul(matmul(EB, Cf), G0);
696 const Matrix<double> corr = matmul(matmul(Cf, matmul(G0, pinv(Mb))), EB);
697 for (std::size_t i = 0; i < nc; ++i)
698 for (std::size_t j = 0; j < nc; ++j) T(i, j) -= corr(i, j);
699 }
700
701 std::vector<std::vector<double>> R;
702 for (std::size_t c : active) {
703 std::vector<double> row(nc, 0.0);
704 for (std::size_t i = 0; i < nc; ++i) row[i] = con.A(c, idx[i]);
705 R.push_back(row);
706 }
707 if (th != nullptr) {
708 for (std::size_t j : terms.latch_mode) {
709 std::vector<double> row(nc, 0.0);
710 for (std::size_t z : terms.modes[j].zblk) {
711 const std::ptrdiff_t at = index_of(idx, z);
712 if (at >= 0) row[static_cast<std::size_t>(at)] = 1.0;
713 }
714 for (std::size_t q = 0; q < th->dslot[j].size(); ++q) {
715 const std::ptrdiff_t at = index_of(idx, th->dslot[j][q]);
716 if (at >= 0) row[static_cast<std::size_t>(at)] -= th->dval[j][q];
717 }
718 R.push_back(row);
719 }
720 }
721 if (!R.empty()) {
722 bool any = false;
723 for (const std::vector<double>& row : R)
724 for (double v : row)
725 if (std::fabs(v) > 1e-14) any = true;
726 if (any) {
727 Matrix<double> Rm(R.size(), nc, 0.0);
728 for (std::size_t i = 0; i < R.size(); ++i)
729 for (std::size_t j = 0; j < nc; ++j) Rm(i, j) = R[i][j];
730 Matrix<double> Rt(nc, R.size(), 0.0);
731 for (std::size_t i = 0; i < R.size(); ++i)
732 for (std::size_t j = 0; j < nc; ++j) Rt(j, i) = Rm(i, j);
733 const Matrix<double> RRt = matmul(Rm, Rt);
734 const Matrix<double> corr = matmul(matmul(Rt, pinv(RRt)), Rm);
735 Matrix<double> P(nc, nc, 0.0);
736 for (std::size_t i = 0; i < nc; ++i)
737 for (std::size_t j = 0; j < nc; ++j) P(i, j) = (i == j ? 1.0 : 0.0) - corr(i, j);
738 T = matmul(P, T);
739 }
740 }
741 double worst = 0.0;
742 for (std::size_t i = 0; i < nc; ++i)
743 for (std::size_t j = 0; j < nc; ++j)
744 worst = std::max(worst, std::fabs(T(i, j) - (i == j ? 1.0 : 0.0)));
745 return worst > 1e-14;
746}
747
748/**
749 * One Lyapunov solve, over the marking coordinates.
750 *
751 * THE DIFFUSION COUNTS THE STOCHASTIC EVENTS ONLY: an immediate flow is not a
752 * Poisson stream with an intensity but the limit of an infinitely fast one whose
753 * fluctuation is slaved, and its pinned coordinate is projected out.
754 */
755inline Matrix<double> sigma_of(const PetriTerms& terms, const Matrix<double>& A,
756 const std::vector<double>& r, const Matrix<double>* clampT) {
757 const std::vector<std::size_t>& idx = terms.cov_idx;
758 const std::size_t nc = idx.size(), ns = terms.stoch_col.size();
759 Matrix<double> Dc(nc, std::max<std::size_t>(ns, 1), 0.0);
760 for (std::size_t i = 0; i < nc; ++i)
761 for (std::size_t j = 0; j < ns; ++j) Dc(i, j) = terms.D(idx[i], terms.stoch_col[j]);
762 Matrix<double> Am(nc, nc, 0.0);
763 for (std::size_t i = 0; i < nc; ++i)
764 for (std::size_t j = 0; j < nc; ++j) Am(i, j) = A(idx[i], idx[j]);
765 if (clampT != nullptr) {
766 // BOTH the jump directions and the generator are reduced: reducing Dc
767 // alone would fix the subspace but leave the generator's orthogonal
768 // component on it, which is not the reduced dynamics when the reduction
769 // is oblique.
770 Dc = matmul(*clampT, Dc);
771 Am = matmul(*clampT, Am);
772 }
773 Matrix<double> Q(nc, nc, 0.0);
774 for (std::size_t i = 0; i < nc; ++i)
775 for (std::size_t j = 0; j < nc; ++j) {
776 double v = 0.0;
777 for (std::size_t e = 0; e < ns; ++e)
778 v += Dc(i, e) * r[terms.stoch_col[e]] * Dc(j, e);
779 Q(i, j) = v;
780 }
781 FluidLyapunovInfo info;
782 const Matrix<double> Sc = fluid_lyapunov(Am, Q, Dc, info);
783 Matrix<double> Sigma(terms.nstate, terms.nstate, 0.0);
784 for (std::size_t i = 0; i < nc; ++i)
785 for (std::size_t j = 0; j < nc; ++j) Sigma(idx[i], idx[j]) = Sc(i, j);
786 return Sigma;
787}
788
789/** The stacked residual, the rates it was evaluated at, and the covariance. */
790struct Residual {
791 bool ok = false;
792 std::vector<double> G, r;
793 PetriTheta th;
794 Matrix<double> Sigma;
795};
796
797/**
798 * The coupled algebraic system, stacked.
799 *
800 * `ok` is false when the closure cannot be evaluated at this iterate, so the line
801 * search can back off; the first evaluation of a pass runs with `quiet=false`,
802 * where a genuine failure surfaces.
803 */
804inline Residual residual(const std::vector<double>& u, const Ctx& ctx, bool quiet) {
805 const PetriTerms& terms = *ctx.terms;
806 const PetriImmediate& imm = *ctx.imm;
807 const std::size_t n = terms.nstate;
808 const Unpacked up = unpack(u, terms, imm, ctx.active.size());
809
810 Residual res;
811 res.th = petri_theta(terms, up.x, up.s2);
812 res.r = petri_rates(terms, up.x, up.phi, up.mu, res.th);
813
814 // the deposit gate of every binding capacity, as a product of fractions
815 std::vector<double> gain(n, 1.0);
816 for (std::size_t k = 0; k < ctx.active.size(); ++k)
817 for (std::size_t s = 0; s < n; ++s)
818 if (ctx.con->cover[ctx.active[k]][s]) gain[s] *= up.zeta[k];
819 std::vector<double> drift(n, 0.0);
820 for (std::size_t s = 0; s < n; ++s) {
821 double neg = 0.0, pos = 0.0;
822 for (std::size_t e = 0; e < terms.nev; ++e) {
823 neg += ctx.Dn(s, e) * res.r[e];
824 pos += ctx.Dp(s, e) * res.r[e];
825 }
826 drift[s] = neg + gain[s] * pos;
827 }
828
829 try {
830 const Matrix<double> A = petri_jacobian(terms, res.th);
831 Matrix<double> T;
832 const bool reduced =
833 clamp_tangent(terms, imm, *ctx.con, ctx.active, &res.th, T);
834 res.Sigma = sigma_of(terms, A, res.r, reduced ? &T : nullptr);
835 } catch (const Error&) {
836 if (!quiet) throw;
837 res.ok = false;
838 return res;
839 }
840
841 std::vector<double> G;
842 G.reserve(n + ctx.C.rows() + terms.npair + imm.rows.size() + terms.latch_mode.size() +
843 ctx.active.size());
844 for (double d : drift) G.push_back(d);
845 for (std::size_t r = 0; r < ctx.C.rows(); ++r) {
846 double v = 0.0;
847 for (std::size_t s = 0; s < n; ++s) v += ctx.C(r, s) * up.x[s];
848 G.push_back(v - ctx.N[r]);
849 }
850 for (std::size_t p = 0; p < terms.npair; ++p)
851 G.push_back(up.s2[p] - res.Sigma(terms.cov_pairs[p].first, terms.cov_pairs[p].second));
852 for (const PetriImmediate::Row& row : imm.rows) {
853 if (row.kind == PetriImmediate::PIN) G.push_back(up.x[row.a]);
854 else if (row.kind == PetriImmediate::RATIO)
855 G.push_back(up.phi[row.a] * row.wb - up.phi[row.b] * row.wa);
856 else G.push_back(up.phi[row.a]);
857 }
858 for (std::size_t j : terms.latch_mode) {
859 double s = 0.0;
860 for (std::size_t z : terms.modes[j].zblk) s += up.x[z];
861 G.push_back(s - res.th.theta[j]);
862 }
863 for (std::size_t k = 0; k < ctx.active.size(); ++k) {
864 double v = 0.0;
865 for (std::size_t s = 0; s < n; ++s) v += ctx.con->A(ctx.active[k], s) * up.x[s];
866 G.push_back(v - ctx.con->b[ctx.active[k]]);
867 }
868 res.G = G;
869 res.ok = true;
870 return res;
871}
872
873inline double inf_norm(const std::vector<double>& v) {
874 double m = 0.0;
875 for (double d : v) m = std::max(m, std::fabs(d));
876 return m;
877}
878
879/**
880 * An iterate projected onto its feasible box, the lower bound only.
881 *
882 * The bound is a VECTOR, one entry per unknown, never a count: a scalar "nfree"
883 * is indistinguishable from a one-unknown bound vector, which is how the MATLAB
884 * twin crashed on the simplest net in the tree.
885 */
886inline std::vector<double> project(const std::vector<double>& u, const std::vector<double>& lb) {
887 if (lb.empty()) return u;
888 if (lb.size() != u.size())
889 throw InputError("fluid_petri: the bound vector has " + std::to_string(lb.size()) +
890 " entries for " + std::to_string(u.size()) + " unknowns");
891 std::vector<double> w = u;
892 for (std::size_t i = 0; i < w.size(); ++i)
893 if (std::isfinite(lb[i])) w[i] = std::max(lb[i], w[i]);
894 return w;
895}
896
897/** Forward-difference Jacobian of the residual. */
898inline Matrix<double> fdjac(const Ctx& ctx, const std::vector<double>& u,
899 const std::vector<double>& G) {
900 const std::size_t n = u.size(), m = G.size();
901 Matrix<double> J(std::max<std::size_t>(m, 1), std::max<std::size_t>(n, 1), 0.0);
902 for (std::size_t k = 0; k < n; ++k) {
903 const double h = 1e-7 * std::max(1.0, std::fabs(u[k]));
904 std::vector<double> up = u;
905 up[k] += h;
906 Residual rp = residual(up, ctx, true);
907 if (rp.ok) {
908 for (std::size_t i = 0; i < m; ++i) J(i, k) = (rp.G[i] - G[i]) / h;
909 continue;
910 }
911 up[k] = u[k] - h;
912 Residual rm = residual(up, ctx, true);
913 if (!rm.ok) continue;
914 for (std::size_t i = 0; i < m; ++i) J(i, k) = (G[i] - rm.G[i]) / h;
915 }
916 return J;
917}
918
919struct NewtonResult {
920 std::vector<double> u;
921 std::size_t iterations = 0;
922 bool converged = false;
923 double resnorm = std::numeric_limits<double>::infinity();
924};
925
926/** Damped projected Newton with a finite-difference Jacobian. */
927inline NewtonResult newton(const Ctx& ctx, const std::vector<double>& u0, double tol,
928 std::size_t maxit, const std::vector<double>& lb) {
929 NewtonResult out;
930 std::vector<double> u = project(u0, lb);
931 Residual res = residual(u, ctx, false);
932 if (!res.ok) {
933 out.u = u;
934 return out;
935 }
936 std::vector<double> G = res.G;
937 double resnorm = inf_norm(G);
938 std::size_t it = 0;
939 for (it = 1; it <= maxit; ++it) {
940 if (resnorm <= tol) {
941 out.u = u;
942 out.iterations = it - 1;
943 out.converged = true;
944 out.resnorm = resnorm;
945 return out;
946 }
947 const Matrix<double> J = fdjac(ctx, u, G);
948 Matrix<double> rhs(G.size(), 1, 0.0);
949 for (std::size_t i = 0; i < G.size(); ++i) rhs(i, 0) = -G[i];
950 const Matrix<double> du = matmul(pinv(J), rhs);
951 double lam = 1.0;
952 bool improved = false;
953 for (int b = 0; b < 30; ++b) {
954 std::vector<double> un(u.size(), 0.0);
955 for (std::size_t i = 0; i < u.size(); ++i) un[i] = u[i] + lam * du(i, 0);
956 un = project(un, lb);
957 Residual rn = residual(un, ctx, true);
958 if (rn.ok) {
959 const double v = inf_norm(rn.G);
960 if (v < resnorm) {
961 u = un;
962 G = rn.G;
963 resnorm = v;
964 improved = true;
965 break;
966 }
967 }
968 lam *= 0.5;
969 }
970 if (!improved) break;
971 }
972 out.u = u;
973 out.iterations = it;
974 out.converged = resnorm <= tol;
975 out.resnorm = resnorm;
976 return out;
977}
978
979/**
980 * The first-order drift: the same rates at zero variance, with an immediate mode
981 * firing at LAM times its enabling degree and its firing weight, and the server
982 * latch relaxed at LAM towards the enabling degree instead of solved. Both are
983 * approximations of an algebraic constraint by a fast reaction, and both are
984 * confined to the seed.
985 */
986inline std::vector<double> seed_drift(const PetriTerms& terms, const std::vector<double>& xin,
987 double lam) {
988 std::vector<double> x = xin;
989 for (std::size_t i = 0; i < x.size(); ++i) x[i] = std::max(0.0, x[i]);
990 const PetriTheta th =
991 petri_theta(terms, x, std::vector<double>(std::max<std::size_t>(terms.npair, 1), 0.0));
992 std::vector<double> phi(terms.imm_idx.size(), 0.0);
993 for (std::size_t k = 0; k < phi.size(); ++k) {
994 const std::size_t j = terms.imm_idx[k];
995 phi[k] = lam * terms.modes[j].weight * th.theta[j];
996 }
997 std::vector<double> mu(terms.latch_mode.size(), 0.0);
998 for (std::size_t q = 0; q < mu.size(); ++q) {
999 const std::size_t j = terms.latch_mode[q];
1000 double s = 0.0;
1001 for (std::size_t z : terms.modes[j].zblk) s += x[z];
1002 mu[q] = lam * (th.theta[j] - s);
1003 }
1004 const std::vector<double> r = petri_rates(terms, x, phi, mu, th);
1005 std::vector<double> d(terms.nstate, 0.0);
1006 for (std::size_t s = 0; s < terms.nstate; ++s) {
1007 double v = 0.0;
1008 for (std::size_t e = 0; e < terms.nev; ++e) v += terms.D(s, e) * r[e];
1009 d[s] = v;
1010 }
1011 return d;
1012}
1013
1014} // namespace solver_detail
1015
1016/**
1017 * Fluid analysis of a stochastic Petri net.
1018 *
1019 * @param sn a model whose nodes are Places, Transitions, Sources and Sinks only
1020 * @param opt the state-size cap, the Newton tolerance and its iteration cap
1021 */
1022template <class T>
1024 const PetriOptions& opt = PetriOptions()) {
1025 const std::size_t M = sn.nstations, K = sn.nclasses;
1026
1027 const PetriVerdict v = petri_applicable(sn);
1028 if (!v.ok)
1029 throw UnsupportedError("solver_fluid_petri: the fluid Petri route cannot solve this "
1030 "model: " + v.reason + ".");
1031
1032 const PetriTerms terms = petri_build_terms(sn);
1033 const std::size_t n = terms.nstate, npair = terms.npair;
1034 if (n > opt.dae_maxstate)
1035 throw UnsupportedError(
1036 "solver_fluid_petri: the fluid Petri route solves a " + std::to_string(n) +
1037 "-unknown algebraic system with a finite-difference Jacobian, above the limit of " +
1038 std::to_string(opt.dae_maxstate) +
1039 " set by options.config.dae_maxstate. Raise that limit, or use SolverSSA for a net "
1040 "of this size");
1041
1042 const PetriConservation cons = petri_conservation(terms);
1043 if (cons.leak > 1e-7)
1044 throw NumericError("solver_fluid_petri: the conserved directions and the jump matrix "
1045 "disagree: the largest leak per unit rate is " +
1046 std::to_string(cons.leak) + ", where it must be zero");
1047 const PetriConstraints con = petri_constraints(sn, terms);
1048 const std::size_t ncon = con.b.size();
1049
1050 // ---- seed ---------------------------------------------------------------
1051 // Newton needs a point in the basin, not an answer. The immediate modes get a
1052 // large FINITE rate here and only here, scaled to the model's own timescale
1053 // rather than taken from GlobalConstants.Immediate: 1e8 against a rate of
1054 // order one is a stiffness the seed does not need, and the answer does not
1055 // depend on the seed's accuracy.
1056 double rmax = 0.0;
1057 for (std::size_t j : terms.timed_idx) {
1058 double s = 0.0;
1059 for (double d : terms.modes[j].d1) s += d;
1060 rmax = std::max(rmax, s);
1061 }
1062 for (std::size_t e = 0; e < terms.nev; ++e)
1063 if (terms.ev_kind[e] == 3) rmax = std::max(rmax, terms.rate_base[e]);
1064 if (!(rmax > 0)) rmax = 1.0;
1065 const double lam = std::min(1e8, 1e4 * rmax);
1066 double mass = 0.0;
1067 for (std::size_t s = 0; s < terms.nm; ++s) mass += terms.x0[s];
1068 double Thor = 50.0 * (mass + 1.0) / rmax;
1069
1070 const auto drift_fn = [&terms, lam](const double&, const std::vector<double>& y) {
1071 return solver_detail::seed_drift(terms, y, lam);
1072 };
1073 std::vector<double> tseed;
1074 std::vector<std::vector<double>> xseed;
1075 for (int attempt = 0; attempt < 6; ++attempt) {
1077 oo.rtol = 1e-6;
1078 oo.atol = 1e-10;
1079 oo.store_trajectory = true;
1080 oo.max_steps = 200000;
1081 const OdeSolution<double> sol =
1082 ode_rosenbrock4(drift_fn, 0.0, Thor, terms.x0, oo);
1083 tseed = sol.t;
1084 xseed = sol.y;
1085 const std::vector<double> d = solver_detail::seed_drift(terms, xseed.back(), lam);
1086 if (solver_detail::inf_norm(d) <= 1e-6 * std::max(1.0, rmax * (mass + 1.0))) break;
1087 Thor *= 4.0;
1088 }
1089 std::vector<double> x = xseed.back();
1090
1091 PetriImmediate imm;
1092 petri_immediate(terms, x, imm);
1093 const std::size_t nimm = imm.n;
1094
1095 // THE VARIANCE IS SEEDED POSITIVE: sigma2 = 0 is where min() has no
1096 // derivative, and a saturated net's first-order fixed point sits there.
1097 std::vector<double> s2(npair, 0.0);
1098 std::vector<bool> ondiag(npair, false);
1099 for (std::size_t p = 0; p < npair; ++p) {
1100 ondiag[p] = (terms.cov_pairs[p].first == terms.cov_pairs[p].second);
1101 if (ondiag[p]) s2[p] = std::max(petri_fine_tol(), x[terms.cov_pairs[p].first]);
1102 }
1103
1104 // ---- steady state -------------------------------------------------------
1105 std::vector<std::size_t> active;
1106 std::vector<double> phi(nimm, 0.0);
1107 const std::size_t nlatch = terms.latch_mode.size();
1108 std::vector<double> mu(nlatch, 0.0), zeta;
1109 std::size_t iters = 0;
1110 const std::size_t aset_max = std::max<std::size_t>(6, 2 * (ncon + nimm) + 2);
1111 bool converged = false;
1112 double resnorm = std::numeric_limits<double>::infinity();
1113 bool have_best = false;
1114 std::vector<double> bx, bs2, bphi, bmu, bzeta;
1115 std::vector<std::size_t> bactive;
1116 PetriImmediate bimm;
1117 double bres = std::numeric_limits<double>::infinity();
1118 bool bconv = false;
1119
1120 for (std::size_t sweep = 0; sweep < aset_max; ++sweep) {
1121 const solver_detail::Ctx ctx =
1122 solver_detail::context(terms, cons, con, imm, active);
1123 std::vector<double> u0;
1124 u0.insert(u0.end(), x.begin(), x.end());
1125 u0.insert(u0.end(), s2.begin(), s2.end());
1126 u0.insert(u0.end(), phi.begin(), phi.end());
1127 u0.insert(u0.end(), mu.begin(), mu.end());
1128 u0.insert(u0.end(), active.size(), 1.0);
1129 std::vector<double> lb(u0.size(), -std::numeric_limits<double>::infinity());
1130 for (std::size_t k = 0; k < nimm; ++k) lb[n + npair + k] = 0.0;
1131 for (std::size_t k = 0; k < active.size(); ++k)
1132 lb[n + npair + nimm + nlatch + k] = 0.0;
1133 for (std::size_t p = 0; p < npair; ++p)
1134 if (ondiag[p]) lb[n + p] = 0.0;
1135
1136 const solver_detail::NewtonResult nr =
1137 solver_detail::newton(ctx, u0, opt.tol, opt.newton_max, lb);
1138 iters += nr.iterations;
1139 resnorm = nr.resnorm;
1140 converged = nr.converged;
1141 const solver_detail::Unpacked up =
1142 solver_detail::unpack(nr.u, terms, imm, active.size());
1143 x = up.x;
1144 s2 = up.s2;
1145 phi = up.phi;
1146 mu = up.mu;
1147 zeta = up.zeta;
1148
1149 if (!have_best || resnorm < bres) {
1150 bx = x; bs2 = s2; bphi = phi; bmu = mu; bzeta = zeta;
1151 bactive = active; bimm = imm; bres = resnorm; bconv = converged;
1152 have_best = true;
1153 }
1154
1155 // The active-set moves, in the order that a failure of each invalidates
1156 // the next: a negative flow means the mode does not fire at all, a
1157 // negative marking means the wrong coordinate was pinned, and only then
1158 // is it worth asking which capacity rows bind.
1159 bool moved = false;
1160 if (nimm > 0) {
1161 double scale = 1.0;
1162 for (double p : phi) scale = std::max(scale, std::fabs(p));
1163 const double thr = -std::max(opt.tol, 1e-10) * scale;
1164 for (std::size_t k = 0; k < nimm && !moved; ++k)
1165 if (phi[k] < thr) {
1166 imm.active[k] = false;
1167 petri_immediate(terms, x, imm);
1168 moved = true;
1169 }
1170 }
1171 if (!moved && nimm > 0) {
1172 const double thr = -std::max(opt.tol, 1e-10);
1173 for (std::size_t s = 0; s < terms.nm && !moved; ++s) {
1174 if (x[s] >= thr) continue;
1175 for (std::size_t k = 0; k < nimm; ++k) {
1176 const PetriMode& md = terms.modes[terms.imm_idx[k]];
1177 if (imm.active[k] &&
1178 std::find(md.arc_slot.begin(), md.arc_slot.end(), s) !=
1179 md.arc_slot.end() &&
1180 imm.bind[k] != static_cast<std::ptrdiff_t>(s)) {
1181 imm.bind[k] = static_cast<std::ptrdiff_t>(s);
1182 petri_immediate(terms, x, imm);
1183 moved = true;
1184 break;
1185 }
1186 }
1187 }
1188 }
1189 if (!moved && ncon > 0) {
1190 std::vector<std::size_t> over;
1191 for (std::size_t c = 0; c < ncon; ++c) {
1192 double val = 0.0;
1193 for (std::size_t s = 0; s < n; ++s) val += con.A(c, s) * x[s];
1194 if (val > con.b[c] + std::max(1e-9, opt.tol) &&
1195 std::find(active.begin(), active.end(), c) == active.end())
1196 over.push_back(c);
1197 }
1198 if (!over.empty()) {
1199 active.insert(active.end(), over.begin(), over.end());
1200 moved = true;
1201 } else {
1202 std::vector<std::size_t> keep;
1203 for (std::size_t i = 0; i < active.size(); ++i)
1204 if (!(i < zeta.size() && zeta[i] > 1 + std::max(1e-9, opt.tol)))
1205 keep.push_back(active[i]);
1206 if (keep.size() != active.size()) {
1207 active = keep;
1208 moved = true;
1209 }
1210 }
1211 }
1212 if (!moved) break;
1213 }
1214
1215 PetriSolution out;
1216 if (have_best && !converged && bconv) {
1217 x = bx; s2 = bs2; phi = bphi; mu = bmu; zeta = bzeta;
1218 active = bactive; imm = bimm; converged = true; resnorm = bres;
1219 }
1220 if (!converged)
1221 out.warnings.push_back(
1222 "The simultaneous closure solve stopped at residual " + std::to_string(resnorm) +
1223 " after " + std::to_string(iters) + " Newton steps without reaching " +
1224 std::to_string(opt.tol) + ". The reported point is the last iterate.");
1225 // A NEGATIVE MARKING IS NOT ROUNDING: the fixed point wanted mass a place
1226 // cannot supply and no immediate mode could be rebound to pin it, so the
1227 // answer is outside the model's own state space and is reported as such.
1228 for (std::size_t s = 0; s < terms.nm; ++s)
1229 if (x[s] < -std::max(opt.tol, 1e-10)) {
1230 out.warnings.push_back(
1231 "The fixed point holds " + std::to_string(x[s]) + " tokens at " +
1232 terms.names_node[terms.coord_node[s]] +
1233 ", which is negative: no immediate transition could be rebound to pin that "
1234 "place at zero. Use SolverCTMC, SolverSSA or SolverLDES for this net.");
1235 break;
1236 }
1237
1238 const solver_detail::Ctx ctx = solver_detail::context(terms, cons, con, imm, active);
1239 std::vector<double> ufin;
1240 ufin.insert(ufin.end(), x.begin(), x.end());
1241 ufin.insert(ufin.end(), s2.begin(), s2.end());
1242 ufin.insert(ufin.end(), phi.begin(), phi.end());
1243 ufin.insert(ufin.end(), mu.begin(), mu.end());
1244 ufin.insert(ufin.end(), zeta.begin(), zeta.end());
1245 const solver_detail::Residual fin = solver_detail::residual(ufin, ctx, false);
1246
1247 // ---- the metrics --------------------------------------------------------
1248 // A PLACE IS AN INF STATION: its queue length is its mean token count and its
1249 // utilization is the same number. Its throughput is the rate at which TOKENS
1250 // leave it, so a consuming mode contributes its firing rate times the arc
1251 // multiplicity -- SolverCTMC's convention, and the one Little's law needs.
1252 out.QN = Matrix<double>(M, K, 0.0);
1253 out.UN = Matrix<double>(M, K, 0.0);
1254 out.RN = Matrix<double>(M, K, 0.0);
1255 out.TN = Matrix<double>(M, K, 0.0);
1256 for (std::size_t s = 0; s < terms.nm; ++s) {
1257 if (terms.coord_station[s] < 0) continue;
1258 const std::size_t i = static_cast<std::size_t>(terms.coord_station[s]);
1259 const std::size_t k = terms.coord_class[s];
1260 out.QN(i, k) += x[s];
1261 out.UN(i, k) = out.QN(i, k);
1262 }
1263 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it =
1264 terms.consumers.begin();
1265 it != terms.consumers.end(); ++it) {
1266 const std::size_t i = it->first / K, k = it->first % K;
1267 const std::vector<double>& w = terms.consumer_w.at(it->first);
1268 double val = 0.0;
1269 for (std::size_t q = 0; q < it->second.size(); ++q) val += w[q] * fin.r[it->second[q]];
1270 out.TN(i, k) += val;
1271 }
1272 for (std::map<std::size_t, std::vector<std::size_t>>::const_iterator it =
1273 terms.producers.begin();
1274 it != terms.producers.end(); ++it) {
1275 const std::size_t i = it->first / K, k = it->first % K;
1276 double val = 0.0;
1277 for (std::size_t e : it->second) val += fin.r[e];
1278 out.TN(i, k) += val;
1279 }
1280 for (std::size_t i = 0; i < M; ++i)
1281 for (std::size_t k = 0; k < K; ++k)
1282 if (out.TN(i, k) > 1e-14) out.RN(i, k) = out.QN(i, k) / out.TN(i, k);
1283
1284 out.t = tseed;
1285 out.xvec_t = xseed;
1286 out.x = x;
1287 out.iters = iters;
1288 out.resnorm = resnorm;
1289 out.converged = converged;
1290 out.Sigma = fin.Sigma;
1291 out.QVar = Matrix<double>(M, K, 0.0);
1292 out.QStd = Matrix<double>(M, K, 0.0);
1293 for (std::size_t s = 0; s < terms.nm; ++s) {
1294 if (terms.coord_station[s] < 0) continue;
1295 const std::size_t i = static_cast<std::size_t>(terms.coord_station[s]);
1296 out.QVar(i, terms.coord_class[s]) = std::max(0.0, fin.Sigma(s, s));
1297 }
1298 for (std::size_t i = 0; i < M; ++i)
1299 for (std::size_t k = 0; k < K; ++k) out.QStd(i, k) = std::sqrt(out.QVar(i, k));
1300
1301 // ---- the report ---------------------------------------------------------
1302 out.petri.marking = Matrix<double>(terms.I, terms.K, 0.0);
1303 out.petri.marking_var = Matrix<double>(terms.I, terms.K, 0.0);
1304 for (std::size_t s = 0; s < terms.nm; ++s) {
1305 out.petri.marking(terms.coord_node[s], terms.coord_class[s]) = x[s];
1306 out.petri.marking_var(terms.coord_node[s], terms.coord_class[s]) =
1307 std::max(0.0, fin.Sigma(s, s));
1308 }
1309 for (const PetriMode& md : terms.modes) out.petri.mode_label.push_back(md.label);
1310 out.petri.mode_flow.assign(terms.modes.size(), 0.0);
1311 for (std::size_t j = 0; j < terms.modes.size(); ++j) {
1312 double val = 0.0;
1313 for (std::size_t e = 0; e < terms.nev; ++e)
1314 if (terms.ev_mode[e] == static_cast<int>(j) &&
1315 (terms.ev_kind[e] == 1 || terms.ev_kind[e] == 4))
1316 val += fin.r[e];
1317 out.petri.mode_flow[j] = val;
1318 }
1319 out.petri.immediate_flow = phi;
1320 out.petri.invariant_label = cons.label;
1321 out.petri.invariant_value = cons.N;
1322 out.petri.invariant_error.assign(cons.C.rows(), 0.0);
1323 for (std::size_t c = 0; c < cons.C.rows(); ++c) {
1324 double val = 0.0;
1325 for (std::size_t s = 0; s < terms.nstate; ++s) val += cons.C(c, s) * x[s];
1326 out.petri.invariant_error[c] = val - cons.N[c];
1327 }
1328 out.petri.capacity_label = con.label;
1329 out.petri.capacity_active = active;
1330 out.petri.capacity_fraction = zeta;
1331 out.petri.pinned = imm.pins;
1332 out.petri.Sigma = fin.Sigma;
1333 return out;
1334}
1335
1336} // namespace petri
1337} // namespace fluid
1338} // namespace line
1339
1340#endif // LINE_SOLVERS_FLUID_PETRI_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
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.
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...
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 ...
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.
PetriTheta petri_theta(const PetriTerms &t, const std::vector< double > &x, const std::vector< double > &s2_in)
The closed enabling term of every mode, and its derivative.
Matrix< 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.
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:341
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
@ Rm
repairman: a single station, rates from the balanced bound
Definition pfqn_mci.h:79
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:304
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...
Integration controls.
Definition ode.h:118
bool store_trajectory
keep every accepted point, not just the last
Definition ode.h:125
T atol
absolute tolerance per component
Definition ode.h:120
std::size_t max_steps
abort after this many accepted steps
Definition ode.h:124
T rtol
relative tolerance per component
Definition ode.h:119
Result of an integration.
Definition ode.h:143
std::vector< std::vector< T > > y
y[i] is the state at t[i]
Definition ode.h:145
std::vector< T > t
accepted time points, t[0] = t0
Definition ode.h:144
The conserved quantities of a net, as equations.
Definition fluid_petri.h:61
std::vector< std::string > label
Definition fluid_petri.h:65
Every finite place capacity as a linear row A x <= b.
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.
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
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< std::vector< std::vector< double > > > QNt
std::vector< std::string > warnings
std::vector< std::vector< double > > xvec_t
std::vector< std::vector< std::vector< double > > > UNt
std::vector< std::vector< std::vector< double > > > TNt
The assembled drift terms of a net.
std::vector< std::size_t > coord_class
std::vector< std::size_t > places
std::size_t nstate
marking coordinates plus phase coordinates
std::vector< std::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::size_t > timed_idx
std::size_t nm
marking coordinates alone
std::vector< std::string > names_node
0-based, one per node
std::vector< std::size_t > coord_node
std::vector< std::pair< std::size_t, std::size_t > > cov_pairs
The Sigma entries the closure reads, and the map back to them.
std::map< std::size_t, std::vector< std::size_t > > producers
std::map< std::size_t, std::vector< std::size_t > > consumers
(station*K + class) -> the events that take tokens out of / into it.
std::map< std::size_t, std::vector< double > > consumer_w
std::vector< std::size_t > latch_mode
std::vector< PetriMode > modes
std::vector< std::ptrdiff_t > coord_station
Matrix< double > D
(nstate x nev) incidence
Whether the fluid Petri route can answer this model, and why not.
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...