LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_pn_firing_rates.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_API_SN_SN_PN_FIRING_RATES_H
6#define LINE_API_SN_SN_PN_FIRING_RATES_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Ports of matlab/src/api/sn/sn_pn_firing_rates.m and sn_pn_avg_rates.m.
12 *
13 * A solver reports a PLACE's throughput as the rate at which its tokens are
14 * consumed; what a stochastic Petri net is actually driven by is the firing
15 * rate of each transition MODE. These two functions invert that: they recover
16 * the mode firing rates x from the reported place throughputs, and then rewrite
17 * the place-level throughput, arrival rate and response time so that the
18 * reported tables are consistent with the token balance.
19 *
20 * THE SYSTEM. Two families of equations, both linear in x.
21 * (a) measurement: for each (place, class) whose consumption is non-empty,
22 * the consumed rate equals the throughput the solver reported. With
23 * tputIsTokens the consumption is weighted by the arc multiplicity, and
24 * without it only by whether the mode touches the place at all -- the
25 * difference between "tokens per unit time" and "firings per unit time",
26 * which is what the two callers respectively hold.
27 * (b) balance: production equals consumption at every place, so the marking
28 * is stationary.
29 * Immediate modes are excluded from the measurement rows (they fire in zero
30 * time, so no measured rate is theirs) but not from the balance rows.
31 * The least-squares solution is the pseudo-inverse, as the reference has it,
32 * and a solution with a materially negative rate is REJECTED rather than
33 * clamped: a negative firing rate means the reported throughputs are not
34 * consistent with any marking-stationary firing vector, and the caller must
35 * keep its own numbers rather than be handed a repaired impossibility.
36 *
37 * GUARDS. The reference refuses an open net (a Source or a Sink present) and a
38 * net in which a place is routed to anything other than a transition, because
39 * neither shape satisfies the balance it is about to impose.
40 *
41 * PER-CLASS ARCS. The reference's enabling and firing tables are (nnodes x
42 * nclasses); this port's `TransitionParam` sums the class dimension away when
43 * the model is read, so a multi-class net cannot be answered here and is
44 * refused by name rather than answered with a class-blind arc.
45 *
46 * ARITHMETIC: double. The pseudo-inverse is an SVD.
47 */
48
49#include <algorithm>
50#include <cmath>
51#include <cstddef>
52#include <string>
53#include <vector>
54
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/svd.h"
59
60namespace line {
61namespace api {
62
63/** What sn_pn_firing_rates returns; `x` empty means "no answer", as in the reference. */
65 std::vector<double> x; ///< firing rate per mode
66 /** consumed[mode][place * nclasses + class], the reference's (mm, pp, k). */
67 std::vector<std::vector<double>> consumed;
68 /** produced[mode][place * nclasses + class], the reference's (mm, pp, k). */
69 std::vector<std::vector<double>> produced;
70 std::vector<std::size_t> place_nodes; ///< 1-based Place node indices
71 std::size_t nclasses = 0; ///< the stride of the two tables above
72};
73
74/**
75 * @brief Ports of matlab/src/api/sn/sn_pn_firing_rates.m and
76 * sn_pn_avg_rates.m.
77 *
78 * @param TN station throughput table, (nstations x nclasses)
79 * @param tput_is_tokens whether TN counts tokens (true) or firings (false)
80 */
81template <class T>
83 bool tput_is_tokens) {
85 const std::size_t R = sn.nclasses, I = sn.nodes.size();
86 std::vector<std::size_t> places, trans;
87 for (std::size_t a = 1; a <= I; ++a) {
88 if (sn.nodes[a - 1].nodetype == qn::NodeType::Place) places.push_back(a);
89 if (sn.nodes[a - 1].nodetype == qn::NodeType::Transition) trans.push_back(a);
90 }
91 if (places.empty() || trans.empty() || TN.rows() == 0) return out;
92 for (std::size_t a = 0; a < I; ++a)
93 if (sn.nodes[a].nodetype == qn::NodeType::Source ||
94 sn.nodes[a].nodetype == qn::NodeType::Sink)
95 return out;
96 // every place must route only to transitions, or the balance does not hold
97 const std::size_t S = sn.nof_stateful();
98 if (sn.rt.rows() != S * R) return out;
99 for (std::size_t pp = 0; pp < places.size(); ++pp) {
100 const std::size_t sfp = sn.stateful_index(places[pp]);
101 if (sfp == 0) return out;
102 for (std::size_t sfj = 1; sfj <= S; ++sfj) {
103 if (sfj == sfp) continue;
104 bool touches = false;
105 for (std::size_t r = 0; r < R && !touches; ++r)
106 for (std::size_t s = 0; s < R; ++s)
107 if (sn.rt((sfp - 1) * R + r, (sfj - 1) * R + s) >
109 sn.rt((sfj - 1) * R + s, (sfp - 1) * R + r) >
111 touches = true;
112 break;
113 }
114 if (touches && sn.nodes[sn.stateful_nodes[sfj - 1] - 1].nodetype !=
115 qn::NodeType::Transition)
116 return out;
117 }
118 }
119
120 std::vector<std::size_t> mode_trans, mode_idx;
121 std::vector<bool> mode_timed;
122 for (std::size_t tt = 0; tt < trans.size(); ++tt) {
123 const std::size_t ind = trans[tt];
124 typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
125 sn.transparam.find(ind);
126 if (it == sn.transparam.end() || it->second.nmodes == 0) return out;
127 for (std::size_t m = 0; m < it->second.nmodes; ++m) {
128 mode_trans.push_back(ind);
129 mode_idx.push_back(m);
130 bool timed = true;
131 if (m < it->second.timing.size())
132 timed = it->second.timing[m] != lang::TimingStrategy::IMMEDIATE;
133 mode_timed.push_back(timed);
134 }
135 }
136 const std::size_t nModes = mode_trans.size();
137 if (nModes == 0) return out;
138
139 out.place_nodes = places;
140 out.nclasses = R;
141 out.consumed.assign(nModes, std::vector<double>(places.size() * R, 0.0));
142 out.produced.assign(nModes, std::vector<double>(places.size() * R, 0.0));
143 for (std::size_t mm = 0; mm < nModes; ++mm) {
144 const qn::TransitionParam<T>& tp = sn.transparam.at(mode_trans[mm]);
145 const Matrix<T>& enab = tp.enabling[mode_idx[mm]];
146 const Matrix<T>& fire = tp.firing[mode_idx[mm]];
147 for (std::size_t pp = 0; pp < places.size(); ++pp) {
148 const std::size_t p = places[pp] - 1;
149 for (std::size_t k = 0; k < R; ++k) {
150 if (p < enab.rows() && k < enab.cols())
151 out.consumed[mm][pp * R + k] =
152 std::max(0.0, num_traits<T>::to_double(enab(p, k)));
153 if (p < fire.rows() && k < fire.cols())
154 out.produced[mm][pp * R + k] =
155 std::max(0.0, num_traits<T>::to_double(fire(p, k)));
156 }
157 }
158 }
159
160 const std::size_t nEq = 2 * places.size() * R;
161 Matrix<double> A(nEq, nModes, 0.0);
162 std::vector<double> b(nEq, 0.0);
163 std::size_t row = 0, nMeasured = 0;
164 for (std::size_t pp = 0; pp < places.size(); ++pp) {
165 const std::size_t ist = sn.nodes[places[pp] - 1].station;
166 for (std::size_t k = 0; k < R; ++k) {
167 std::vector<double> arow(nModes, 0.0);
168 bool any = false;
169 for (std::size_t mm = 0; mm < nModes; ++mm) {
170 const double c = out.consumed[mm][pp * R + k];
171 double v = tput_is_tokens ? c : (c > 0.0 ? 1.0 : 0.0);
172 if (!mode_timed[mm]) v = 0.0;
173 arow[mm] = v;
174 if (v != 0.0) any = true;
175 }
176 if (any) {
177 for (std::size_t mm = 0; mm < nModes; ++mm) A(row, mm) = arow[mm];
178 b[row] = ist == 0 ? 0.0 : num_traits<T>::to_double(TN(ist - 1, k));
179 ++row;
180 ++nMeasured;
181 }
182 for (std::size_t mm = 0; mm < nModes; ++mm)
183 A(row, mm) = out.produced[mm][pp * R + k] - out.consumed[mm][pp * R + k];
184 b[row] = 0.0;
185 ++row;
186 }
187 }
188 if (nMeasured == 0) return out;
189
190 Matrix<double> At(row, nModes, 0.0);
191 for (std::size_t a = 0; a < row; ++a)
192 for (std::size_t m = 0; m < nModes; ++m) At(a, m) = A(a, m);
193 const Matrix<double> Ap = pinv(At);
194 std::vector<double> xfit(nModes, 0.0);
195 for (std::size_t m = 0; m < nModes; ++m) {
196 double acc = 0.0;
197 for (std::size_t a = 0; a < row; ++a) acc += Ap(m, a) * b[a];
198 xfit[m] = acc;
199 }
200 double mx = 1.0;
201 for (std::size_t m = 0; m < nModes; ++m) mx = std::max(mx, std::fabs(xfit[m]));
202 for (std::size_t m = 0; m < nModes; ++m)
203 if (xfit[m] < -1e-6 * mx) return out;
204 out.x = xfit;
205 return out;
206}
207
208/** What sn_pn_avg_rates rewrites in place; empty tables are left empty. */
209template <class T>
213
214/**
215 * Port of sn_pn_avg_rates: rewrite the place rows of TN, AN and RN so that
216 * they agree with the recovered mode firing rates.
217 *
218 * A place's throughput becomes the rate its tokens are CONSUMED at, its
219 * arrival rate the rate they are PRODUCED at, and its response time the
220 * quotient the queue length and that throughput define. A model with no Place
221 * node, or one whose firing rates could not be recovered, is returned
222 * untouched.
223 */
224template <class T>
226 const Matrix<T>& TN, const Matrix<T>& AN, const Matrix<T>& RN) {
227 SnPnAvgRates<T> out;
228 out.TN = TN;
229 out.AN = AN;
230 out.RN = RN;
231 if (TN.rows() == 0) return out;
232 bool has_place = false;
233 for (std::size_t a = 0; a < sn.nodes.size(); ++a)
234 if (sn.nodes[a].nodetype == qn::NodeType::Place) has_place = true;
235 if (!has_place) return out;
236 const SnPnFiringRates fr = sn_pn_firing_rates(sn, TN, false);
237 if (fr.x.empty()) return out;
238 const std::size_t R = sn.nclasses;
239 for (std::size_t pp = 0; pp < fr.place_nodes.size(); ++pp) {
240 const std::size_t ist = sn.nodes[fr.place_nodes[pp] - 1].station;
241 if (ist == 0) continue;
242 for (std::size_t k = 0; k < R; ++k) {
243 double tk = 0.0, ak = 0.0;
244 for (std::size_t m = 0; m < fr.x.size(); ++m) {
245 tk += fr.consumed[m][pp * R + k] * fr.x[m];
246 ak += fr.produced[m][pp * R + k] * fr.x[m];
247 }
248 out.TN(ist - 1, k) = num_traits<T>::from_double(tk);
249 if (out.AN.rows() != 0) out.AN(ist - 1, k) = num_traits<T>::from_double(ak);
250 if (out.RN.rows() != 0)
251 out.RN(ist - 1, k) =
252 tk > 0.0 ? T(QN(ist - 1, k) / num_traits<T>::from_double(tk))
254 }
255 }
256 return out;
257}
258
259} // namespace api
260} // namespace line
261
262#endif // LINE_API_SN_SN_PN_FIRING_RATES_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
The exception types the port throws.
SnPnFiringRates sn_pn_firing_rates(const qn::NetworkStruct< T > &sn, const Matrix< T > &TN, bool tput_is_tokens)
Ports of matlab/src/api/sn/sn_pn_firing_rates.m and sn_pn_avg_rates.m.
SnPnAvgRates< T > sn_pn_avg_rates(const qn::NetworkStruct< T > &sn, const Matrix< T > &QN, const Matrix< T > &TN, const Matrix< T > &AN, const Matrix< T > &RN)
Port of sn_pn_avg_rates: rewrite the place rows of TN, AN and RN so that they agree with the recovere...
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
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
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
What sn_pn_avg_rates rewrites in place; empty tables are left empty.
What sn_pn_firing_rates returns; x empty means "no answer", as in the reference.
std::vector< std::vector< double > > produced
produced[mode][place * nclasses + class], the reference's (mm, pp, k).
std::vector< std::vector< double > > consumed
consumed[mode][place * nclasses + class], the reference's (mm, pp, k).
std::vector< double > x
firing rate per mode
std::vector< std::size_t > place_nodes
1-based Place node indices
std::size_t nclasses
the stride of the two tables above
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< Matrix< T > > firing
firing[m](p,r): class-r tokens mode m moves to/from place p when it fires.
std::vector< Matrix< T > > enabling
enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs.
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...