5#ifndef LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPN_H
6#define LINE_SOLVERS_SSA_SOLVER_SSA_NRM_SPN_H
95 std::vector<double> S;
96 std::vector<std::size_t> en_slot;
97 std::vector<double> en_w;
98 std::vector<std::size_t> inh_slot;
99 std::vector<double> inh_thr;
100 double base_rate = 0.0;
101 double nservers = 1.0;
104 std::vector<std::size_t> dep_slots;
113inline double spn_en_degree(
const std::vector<double>& n,
const SpnReaction& rx) {
114 for (std::size_t i = 0; i < rx.inh_slot.size(); ++i)
115 if (n[rx.inh_slot[i]] >= rx.inh_thr[i])
return 0.0;
116 if (rx.en_slot.empty())
return 1.0;
117 double d = std::numeric_limits<double>::infinity();
118 for (std::size_t i = 0; i < rx.en_slot.size(); ++i)
119 d = std::min(d, std::floor(n[rx.en_slot[i]] / rx.en_w[i]));
129inline double spn_propensity(
const std::vector<double>& n,
const SpnReaction& rx) {
130 const double eff = std::min(spn_en_degree(n, rx), rx.nservers);
131 return eff <= 0.0 ? 0.0 : rx.base_rate * eff;
153bool ssa_check_firingdep(
const qn::NetworkStruct<T>& sn,
bool raise =
true) {
154 for (
typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
155 sn.transparam.begin();
156 it != sn.transparam.end(); ++it) {
157 const qn::TransitionParam<T>& tp = it->second;
158 for (std::size_t m = 0; m < tp.nmodes && m < tp.firingdep.size(); ++m) {
159 if (!tp.firingdep[m])
continue;
160 if (!raise)
return false;
161 const std::size_t ind = it->first;
163 "SolverSSA: transition '" +
164 (ind >= 1 && ind <= sn.nodes.size() ? sn.nodes[ind - 1].name : std::string(
"?")) +
165 "' mode " + std::to_string(m + 1) +
166 " uses a marking-dependent firing rate (setFiringRateDependence), which the "
167 "NRM does not support: it builds one constant-propensity reaction per timed "
168 "mode and never evaluates the handle. Ask for method='serial', which applies "
169 "it, or use SolverCTMC or SolverLDES");
185bool spn_nrm_supported(
const qn::NetworkStruct<T>& sn,
bool raise =
true) {
186 const std::size_t I = sn.nodes.size();
187 const std::size_t K = sn.nclasses;
188 bool any_transition =
false;
189 for (std::size_t i = 0; i < I; ++i)
190 if (sn.nodes[i].nodetype == qn::NodeType::Transition) any_transition =
true;
191 if (!any_transition)
return true;
192 if (!ssa_check_firingdep(sn, raise))
return false;
194 for (std::size_t i = 0; i < I; ++i) {
195 if (sn.nodes[i].nodetype == qn::NodeType::Transition) {
196 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
197 sn.transparam.find(i + 1);
198 if (it == sn.transparam.end())
continue;
199 const qn::TransitionParam<T>& tp = it->second;
200 for (std::size_t m = 0; m < tp.nmodes; ++m) {
203 const bool one_phase = m < tp.firingphases.size() && tp.firingphases[m] == 1;
204 const bool has_proc = m < tp.firingproc.size() && tp.firingproc[m].D1.rows() > 0;
205 if (one_phase && has_proc)
continue;
206 if (!raise)
return false;
208 "SolverSSA(method='nrm'): transition '" + sn.nodes[i].name +
"' mode " +
209 std::to_string(m + 1) +
210 " has a non-exponential timed firing. The reaction network carries one "
211 "constant-rate reaction per mode and no per-mode phase, so an in-flight "
212 "firing has nowhere to keep its phase; use method='serial'");
214 }
else if (sn.nodes[i].nodetype == qn::NodeType::Source) {
215 const std::size_t ist = sn.nodes[i].station;
216 if (ist == 0)
continue;
217 for (std::size_t r = 0; r < K; ++r) {
218 const double lambda = num_traits<T>::to_double(sn.rates(ist - 1, r));
219 if (std::isnan(lambda) || lambda <= 0.0)
continue;
221 if (!raise)
return false;
223 "SolverSSA(method='nrm'): source '" + sn.nodes[i].name +
"' class '" +
225 "' has a non-exponential arrival. An arrival into a Place is one "
226 "constant-propensity reaction here, which only a Poisson stream is; "
227 "use method='serial'");
230 for (std::size_t j = 0; j < I && !feeds; ++j) {
231 if (sn.nodes[j].nodetype != qn::NodeType::Place)
continue;
232 for (std::size_t s = 0; s < K && !feeds; ++s)
233 if (num_traits<T>::to_double(sn.rtnodes(i * K + r, j * K + s)) > 0.0)
237 if (!raise)
return false;
239 "SolverSSA(method='nrm'): source '" + sn.nodes[i].name +
"' class '" +
241 "' routes to no Place. The SPN path deposits an arrival into a Place "
242 "slot and has nowhere else to put one; use method='serial'");
245 }
else if (sn.nodes[i].nodetype == qn::NodeType::Place) {
246 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
247 sn.initmarking.find(i + 1);
248 if (im == sn.initmarking.end())
continue;
249 for (std::size_t r = 0; r < im->second.size(); ++r) {
250 if (std::isfinite(num_traits<T>::to_double(im->second[r])))
continue;
251 if (!raise)
return false;
252 throw UnsupportedError(
"SolverSSA(method='nrm'): place '" + sn.nodes[i].name +
253 "' declares an infinite initial marking, which the "
254 "reaction network cannot count; use method='serial'");
275 : sn_(
sn), opt_(
opt), rng_(
opt.seed) {
288 using Rx = detail::SpnReaction;
294 std::size_t I_ = 0, K_ = 0, M_ = 0, NS_ = 0;
297 std::vector<Rx> imm_;
299 std::vector<std::vector<std::vector<std::size_t>>> consumers_;
301 std::vector<std::vector<std::vector<std::size_t>>> producers_;
303 std::vector<double> nvec0_;
304 std::vector<double> pcap_slot_;
306 std::vector<std::pair<double, std::vector<std::size_t>>> place_total_caps_;
307 bool has_caps_ =
false;
310 static const std::size_t kMaxImmSteps = 100000;
312 std::size_t slot(std::size_t node0, std::size_t cls)
const {
return node0 * K_ + cls; }
315 Rx build_mode(std::size_t ind0, std::size_t m)
const;
316 void apply_caps(std::vector<double>& n,
const std::vector<std::size_t>& deposited)
const;
317 void collapse(std::vector<double>& n);
329void NrmSpnEngine<T>::build() {
330 I_ = sn_.nodes.size();
334 consumers_.assign(I_, std::vector<std::vector<std::size_t>>(K_));
335 producers_.assign(I_, std::vector<std::vector<std::size_t>>(K_));
341 detail::ssa_check_firingdep(sn_,
true);
343 for (std::size_t ind = 0; ind < I_; ++ind) {
344 if (sn_.nodes[ind].nodetype != qn::NodeType::Transition)
continue;
345 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
346 sn_.transparam.find(ind + 1);
347 if (it == sn_.transparam.end())
continue;
348 const qn::TransitionParam<T>& tp = it->second;
349 for (std::size_t m = 0; m < tp.nmodes; ++m) {
350 const Rx rec = build_mode(ind, m);
356 const std::size_t ridx = rx_.size() - 1;
357 for (std::size_t a = 0; a < rec.en_slot.size(); ++a)
358 consumers_[rec.en_slot[a] / K_][rec.en_slot[a] % K_].push_back(ridx);
367 for (std::size_t ind = 0; ind < I_; ++ind) {
368 if (sn_.nodes[ind].nodetype != qn::NodeType::Source)
continue;
369 const std::size_t ist = sn_.nodes[ind].station;
370 if (ist == 0)
continue;
371 for (std::size_t r = 0; r < K_; ++r) {
372 const double lambda = num_traits<T>::to_double(sn_.rates(ist - 1, r));
373 if (std::isnan(lambda) || lambda <= 0.0)
continue;
376 "solver_ssa_nrm_spn: source '" + sn_.nodes[ind].name +
"' class '" +
377 sn_.classes[r].name +
378 "' has a non-exponential arrival, which the SPN reaction network cannot "
379 "express; use method='serial' or SolverJMT");
380 bool found_place =
false;
381 for (std::size_t jnd = 0; jnd < I_; ++jnd) {
382 if (sn_.nodes[jnd].nodetype != qn::NodeType::Place)
continue;
383 for (std::size_t s = 0; s < K_; ++s) {
385 num_traits<T>::to_double(sn_.rtnodes(ind * K_ + r, jnd * K_ + s));
386 if (p <= 0.0)
continue;
391 rec.S.assign(NS_, 0.0);
392 rec.S[slot(jnd, s)] += 1.0;
393 rec.base_rate = lambda * p;
395 rec.dep_slots.push_back(slot(jnd, s));
397 producers_[ind][r].push_back(rx_.size() - 1);
401 throw UnsupportedError(
"solver_ssa_nrm_spn: source '" + sn_.nodes[ind].name +
402 "' class '" + sn_.classes[r].name +
403 "' does not route to any Place; the SPN path needs a "
404 "Source->Place arc");
410 "solver_ssa_nrm_spn: the stochastic Petri net has no timed reaction; there is "
411 "nothing to simulate");
427 nvec0_.assign(NS_, 0.0);
428 for (std::size_t r = 0; r < K_; ++r) {
429 const double pop = sn_.classes[r].population;
430 if (!std::isfinite(pop) || pop <= 0.0)
continue;
431 const std::size_t rs = sn_.classes[r].refstat;
432 if (rs < 1 || rs > M_)
continue;
433 const std::size_t ind = sn_.station_to_node[rs - 1] - 1;
434 if (sn_.nodes[ind].nodetype != qn::NodeType::Place)
continue;
435 nvec0_[slot(ind, r)] = pop;
437 for (std::size_t ind = 0; ind < I_; ++ind) {
438 if (sn_.nodes[ind].nodetype != qn::NodeType::Place)
continue;
439 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
440 sn_.initmarking.find(ind + 1);
441 if (im == sn_.initmarking.end())
continue;
442 for (std::size_t r = 0; r < K_; ++r) {
444 r < im->second.size() ? num_traits<T>::to_double(im->second[r]) : 0.0;
445 if (!std::isfinite(v))
446 throw UnsupportedError(
"solver_ssa_nrm_spn: place '" + sn_.nodes[ind].name +
447 "' declares an infinite initial marking, which the "
448 "reaction network cannot count");
449 nvec0_[slot(ind, r)] = v > 0.0 ? v : 0.0;
458 const double inf = std::numeric_limits<double>::infinity();
459 pcap_slot_.assign(NS_, inf);
460 for (std::size_t ind = 0; ind < I_; ++ind) {
461 if (sn_.nodes[ind].nodetype != qn::NodeType::Place)
continue;
462 const std::size_t ist = sn_.nodes[ind].station;
463 if (ist == 0)
continue;
464 std::vector<std::size_t> slots_here;
465 for (std::size_t r = 0; r < K_; ++r) {
466 slots_here.push_back(slot(ind, r));
467 if (ist - 1 < sn_.classcap.size() && r < sn_.classcap[ist - 1].size()) {
468 const double cc = sn_.classcap[ist - 1][r];
469 if (std::isfinite(cc)) pcap_slot_[slot(ind, r)] = cc;
472 if (ist - 1 < sn_.cap.size() && std::isfinite(sn_.cap[ist - 1]))
473 place_total_caps_.push_back(std::make_pair(sn_.cap[ist - 1], slots_here));
475 has_caps_ = !place_total_caps_.empty();
476 for (std::size_t j = 0; j < NS_ && !has_caps_; ++j)
477 if (std::isfinite(pcap_slot_[j])) has_caps_ =
true;
490typename NrmSpnEngine<T>::Rx NrmSpnEngine<T>::build_mode(std::size_t ind0, std::size_t m)
const {
491 const qn::TransitionParam<T>& tp = sn_.transparam.at(ind0 + 1);
495 rec.S.assign(NS_, 0.0);
497 if (m < tp.enabling.size()) {
498 const Matrix<T>& en = tp.enabling[m];
499 for (std::size_t p = 0; p < en.rows() && p < I_; ++p)
500 for (std::size_t c = 0; c < en.cols() && c < K_; ++c) {
501 const double w = num_traits<T>::to_double(en(p, c));
502 if (w == 0.0)
continue;
503 rec.en_slot.push_back(slot(p, c));
504 rec.en_w.push_back(w);
505 rec.S[slot(p, c)] -= w;
508 if (m < tp.firing.size()) {
509 const Matrix<T>& fir = tp.firing[m];
510 for (std::size_t p = 0; p < fir.rows() && p < I_; ++p)
511 for (std::size_t c = 0; c < fir.cols() && c < K_; ++c) {
512 const double w = num_traits<T>::to_double(fir(p, c));
513 if (w == 0.0)
continue;
514 rec.S[slot(p, c)] += w;
517 if (m < tp.inhibiting.size()) {
518 const Matrix<T>& inh = tp.inhibiting[m];
519 for (std::size_t p = 0; p < inh.rows() && p < I_; ++p)
520 for (std::size_t c = 0; c < inh.cols() && c < K_; ++c) {
521 const double thr = num_traits<T>::to_double(inh(p, c));
522 if (!std::isfinite(thr))
continue;
523 rec.inh_slot.push_back(slot(p, c));
524 rec.inh_thr.push_back(thr);
527 for (std::size_t j = 0; j < NS_; ++j)
528 if (rec.S[j] > 0.0) rec.dep_slots.push_back(j);
534 const bool one_phase = m < tp.firingphases.size() && tp.firingphases[m] == 1;
535 if (!one_phase || m >= tp.firingproc.size() || tp.firingproc[m].D1.rows() == 0)
536 throw UnsupportedError(
"solver_ssa_nrm_spn: transition '" + sn_.nodes[ind0].name +
537 "' mode " + std::to_string(m + 1) +
538 " has a non-exponential firing, which the SPN reaction "
539 "network cannot express; use method='serial'");
540 const Matrix<T>& d1 = tp.firingproc[m].D1;
542 for (std::size_t a = 0; a < d1.rows(); ++a)
543 for (std::size_t b = 0; b < d1.cols(); ++b) s += num_traits<T>::to_double(d1(a, b));
546 rec.nservers = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
548 rec.weight = m < tp.fireweight.size() ? num_traits<T>::to_double(tp.fireweight[m]) : 1.0;
549 rec.prio = m < tp.firingprio.size() ? tp.firingprio[m] : 1.0;
561void NrmSpnEngine<T>::apply_caps(std::vector<double>& n,
562 const std::vector<std::size_t>& deposited)
const {
563 for (std::size_t a = 0; a < deposited.size(); ++a) {
564 const std::size_t j = deposited[a];
565 if (n[j] > pcap_slot_[j]) n[j] = pcap_slot_[j];
567 for (std::size_t p = 0; p < place_total_caps_.size(); ++p) {
568 const double tcap = place_total_caps_[p].first;
569 const std::vector<std::size_t>& slots = place_total_caps_[p].second;
571 for (std::size_t a = 0; a < slots.size(); ++a) total += n[slots[a]];
572 double excess = total - tcap;
573 for (std::size_t a = 0; a < deposited.size() && excess > 0.0; ++a) {
574 const std::size_t j = deposited[a];
575 if (std::find(slots.begin(), slots.end(), j) == slots.end())
continue;
576 if (n[j] <= 0.0)
continue;
577 const double d = std::min(excess, n[j]);
593void NrmSpnEngine<T>::collapse(std::vector<double>& n) {
594 if (imm_.empty())
return;
595 std::size_t steps = 0;
597 std::vector<std::size_t> enabled;
598 for (std::size_t m = 0; m < imm_.size(); ++m)
599 if (detail::spn_en_degree(n, imm_[m]) >= 1.0) enabled.push_back(m);
600 if (enabled.empty())
return;
601 double top_prio = -std::numeric_limits<double>::infinity();
602 for (std::size_t i = 0; i < enabled.size(); ++i)
603 top_prio = std::max(top_prio, imm_[enabled[i]].prio);
604 std::vector<std::size_t> top;
605 std::vector<double> w;
606 for (std::size_t i = 0; i < enabled.size(); ++i)
607 if (imm_[enabled[i]].prio == top_prio) {
608 top.push_back(enabled[i]);
609 w.push_back(imm_[enabled[i]].weight);
611 const std::size_t pick = top.size() == 1 ? top[0] : top[rng_.draw(w)];
612 for (std::size_t j = 0; j < NS_; ++j) n[j] += imm_[pick].S[j];
613 if (++steps > kMaxImmSteps)
615 "solver_ssa_nrm_spn: immediate-transition livelock -- the vanishing-marking "
616 "collapse did not reach a tangible marking");
634 const std::size_t nrx = rx_.size();
640 out.
CN.assign(K_, 0.0);
641 out.
XN.assign(K_, 0.0);
646 std::vector<double> nvec = nvec0_;
649 std::vector<std::size_t> all(NS_);
650 for (std::size_t j = 0; j < NS_; ++j) all[j] = j;
651 apply_caps(nvec, all);
654 std::vector<double> Ak(nrx, 0.0), Pk(nrx, 0.0), Tk(nrx, 0.0), tau(nrx, 0.0);
655 const double inf = std::numeric_limits<double>::infinity();
656 for (std::size_t k = 0; k < nrx; ++k) {
657 Ak[k] = detail::spn_propensity(nvec, rx_[k]);
658 Pk[k] = -std::log(rng_.uniform());
659 tau[k] = Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : inf;
662 double total_time = 0.0;
665 static_cast<std::size_t
>(opt_.samples));
666 const std::size_t console_every = std::max<std::size_t>(1, opt_.samples / 20);
667 for (; n < opt_.samples; ++n) {
668 if ((n + 1) % console_every == 0)
670 static_cast<long>((n + 1) / console_every),
671 "simulated %zu of %zu samples (%.0f%%), simulated time %.4g", n + 1,
672 static_cast<std::size_t
>(opt_.samples),
673 100.0 *
static_cast<double>(n + 1) /
static_cast<double>(opt_.samples),
675 std::size_t kfire = 0;
677 for (std::size_t k = 0; k < nrx; ++k)
684 "solver_ssa_nrm_spn: deadlock -- no transition is enabled, so the sample path "
688 for (std::size_t ist = 0; ist < M_; ++ist) {
689 const std::size_t ind = sn_.station_to_node[ist] - 1;
690 for (std::size_t c = 0; c < K_; ++c) {
691 const double tokens = nvec[slot(ind, c)];
692 out.
QN(ist, c) += tokens * dt;
693 out.
UN(ist, c) += tokens * dt;
695 const std::vector<std::size_t>& cons = consumers_[ind][c];
696 for (std::size_t a = 0; a < cons.size(); ++a) depr += Ak[cons[a]];
700 const std::vector<std::size_t>& prod = producers_[ind][c];
701 for (std::size_t a = 0; a < prod.size(); ++a) depr += Ak[prod[a]];
702 out.
TN(ist, c) += depr * dt;
709 for (std::size_t j = 0; j < NS_; ++j) nvec[j] += rx_[kfire].S[j];
710 if (has_caps_) apply_caps(nvec, rx_[kfire].dep_slots);
715 for (std::size_t k = 0; k < nrx; ++k) Tk[k] += Ak[k] * dt;
716 for (std::size_t k = 0; k < nrx; ++k) Ak[k] = detail::spn_propensity(nvec, rx_[k]);
717 Pk[kfire] -= std::log(rng_.uniform());
718 for (std::size_t k = 0; k < nrx; ++k)
719 tau[k] = Ak[k] > 0.0 ? (Pk[k] - Tk[k]) / Ak[k] : inf;
722 if (total_time > 0.0)
723 for (std::size_t ist = 0; ist < M_; ++ist)
724 for (std::size_t c = 0; c < K_; ++c) {
725 out.
QN(ist, c) /= total_time;
726 out.
UN(ist, c) /= total_time;
727 out.
TN(ist, c) /= total_time;
729 for (std::size_t c = 0; c < K_; ++c) {
730 out.
XN[c] = out.
TN(sn_.classes[c].refstat - 1, c);
731 for (std::size_t ist = 0; ist < M_; ++ist)
732 out.
RN(ist, c) = out.
TN(ist, c) > 0.0 ? out.
QN(ist, c) / out.
TN(ist, c) : 0.0;
733 if (out.
XN[c] > 0.0) out.
CN[c] = sn_.classes[c].population / out.
XN[c];
NumericError(const std::string &what)
UnsupportedError(const std::string &what)
A network plus its refreshed NetworkStruct.
NrmSpnEngine(const qn::NetworkStruct< T > &sn, const SsaOptions &opt)
SsaSolution run()
Run opt.samples firings and return the time-averaged metrics.
std::size_t nimmediate() const
The immediate-mode count, likewise.
std::size_t nreactions() const
The reaction count, for the tests that assert the builder.
The uniform source, MATLAB's rand.
static void loop(const char *fmt,...)
Announce an iteration loop and reset its reporting budget.
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
The exception types the port throws.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Conservation laws of a layered queueing network, enumerated from its structure.
A queueing network and its refreshed NetworkStruct.
Controls, results and the random source of SolverSSA.
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Controls, defaulting to SolverOptions('SSA') in the reference.
What the analyzer returns, in the same shape as the MVA and fluid results.
Matrix< double > StartN
The DERIVED rates, (nstations x nclasses): how often per unit time a class-r service STARTS at statio...
double simulated_time
Simulated time the metrics are averaged over; the reference's totalTime.
std::size_t samples
Reaction firings actually performed.
std::string method
The concrete algorithm, as the reference's method.
Matrix< double > PreemptN