LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
state_events.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_LANG_QN_STATE_EVENTS_H
6#define LINE_LANG_QN_STATE_EVENTS_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Port of the event half of MATLAB's `+State` package: the successor states an
12 * event produces at one node, with their rates and probabilities. This is what
13 * turns the enumerated state space of `state.h` into a generator.
14 *
15 * THE ACTIVE / PASSIVE CONVENTION. An event is ACTIVE at the node that
16 * schedules it and PASSIVE at the node that receives it -- a DEP at one station
17 * IS the ARV at the next. Only the active half knows a rate, so the passive
18 * half returns the sentinel -1 and the generator assembly substitutes the
19 * active rate. The sentinel is unambiguous because a rate is never negative.
20 *
21 * WHY PROBABILITIES ARE SEPARATE FROM RATES. One event can have several
22 * successors: an entering job picks its service phase, a signal picks the job
23 * it removes, a random-order queue picks whom to serve. The rate belongs to the
24 * event and the probability to the choice, so a single (state, event) pair
25 * yields a ROW of successors and the generator entry is rate * prob.
26 */
27
28#include <cmath>
29#include <cstddef>
30#include <limits>
31#include <algorithm>
32#include <functional>
33#include <utility>
34#include <vector>
35
40#include "line/lang/qn/state.h"
41#include "line/util/error.h"
42
43namespace line {
44namespace qn {
45
46using lang::EventType;
47
48/**
49 * What one event produces at one node: the successor rows, their rates and
50 * their probabilities, all three the same length.
51 */
52template <class T>
54 std::vector<std::vector<T>> space; ///< successor local state rows
55 std::vector<T> rate; ///< per-row rate, -1 on a passive half
56 std::vector<T> prob; ///< per-row probability of the choice
57 /// START annotation: the 1-based classes that BEGIN or RESUME holding a
58 /// server on each successor row. An instantaneous tag on the arc the row
59 /// already carries, never an event of its own, so no rate, probability or
60 /// state depends on it. Usually empty; kept as a list so one arc can start
61 /// several jobs (a region release cascade does).
62 std::vector<std::vector<std::size_t>> start;
63 /// PREEMPT annotation: the 1-based classes pushed back into the buffer.
64 std::vector<std::vector<std::size_t>> preempt;
65 bool empty() const { return space.empty(); }
66};
67
68/**
69 * Tag the successor row just appended to OUT: START_CLS begins service on it
70 * and PREEMPT_CLS is displaced by it, either 0 for none. The tag vectors are
71 * grown to match `space`, so only the arcs that carry a tag need to say
72 * anything and every other row is an empty list.
73 */
74template <typename T>
75inline void tag_last(EventOutcome<T>& out, std::size_t start_cls, std::size_t preempt_cls) {
76 if (out.space.empty()) return;
77 out.start.resize(out.space.size());
78 out.preempt.resize(out.space.size());
79 if (start_cls) out.start.back().push_back(start_cls);
80 if (preempt_cls) out.preempt.back().push_back(preempt_cls);
81}
82
83/**
84 * Bring the tag vectors up to one entry per successor, so a caller can index
85 * them exactly like `space`.
86 */
87template <typename T>
88inline void pad_tags(EventOutcome<T>& out) {
89 out.start.resize(out.space.size());
90 out.preempt.resize(out.space.size());
91}
92
93/**
94 * Port of `State.toMarginalAggr`: the job counts of one node's state row,
95 * without the per-phase detail `to_marginal` also computes.
96 *
97 * It is NOT simply a projection of `to_marginal`: it accepts stateful
98 * non-stations (whose row is a plain per-class count ahead of the local
99 * variables), and it leaves the preemptive families out of its buffer switch,
100 * so for those it reports only the jobs IN SERVICE. That asymmetry is the
101 * reference's, and it is load-bearing -- the arrival branch uses this to test
102 * for room, where counting a preempted job twice would refuse a valid arrival.
103 *
104 * @param sn the network struct
105 * @param ind NODE index (1-based), as in the reference
106 * @param state_i the node's state row
107 * @return (ni, nir): total jobs and jobs per class
108 */
109template <class T>
110std::pair<T, std::vector<T>> to_marginal_aggr(const NetworkStruct<T>& sn, std::size_t ind,
111 const std::vector<T>& state_i) {
112 const std::size_t R = sn.nclasses;
113 const T zero = num_traits<T>::from_int(0);
114 std::vector<T> nir(R, zero);
115 if (ind == 0 || ind > sn.nodes.size())
116 throw InputError("to_marginal_aggr: node index is out of range");
117 const NodeDef& nd = sn.nodes[ind - 1];
118 const std::size_t ist = nd.station;
119 const std::size_t nvar = sn.nvars_of(ind);
120
121 // A Join of an FJ-augmented struct is a station whose row is a bare per-class
122 // count: it holds jobs waiting to synchronize, with no buffer/phase split and
123 // no service at all, so the station path below would read its counts as
124 // buffer tags.
125 if (sn.isfjaugmented && nd.nodetype == NodeType::Join && state_i.size() >= R) {
126 T ni = zero;
127 for (std::size_t r = 0; r < R; ++r) {
128 nir[r] = state_i[state_i.size() - R + r];
129 ni += nir[r];
130 }
131 return std::make_pair(ni, nir);
132 }
133
134 // A stateful non-station carries a per-class count ahead of its local
135 // variables. A node with nothing but bookkeeping (a Router's round-robin
136 // pointer) still has to report R zeros, so callers can index nir[r].
137 if (ist == 0) {
138 const std::size_t bufw = state_i.size() > nvar ? state_i.size() - nvar : 0;
139 for (std::size_t r = 0; r < R && r < bufw; ++r) nir[r] = state_i[r];
140 T ni = zero;
141 for (std::size_t r = 0; r < R; ++r) ni += nir[r];
142 return std::make_pair(ni, nir);
143 }
144
145 // A Source reports zero, not Inf: its jobs are external, and the EXT
146 // sentinel `to_marginal` returns describes the encoding, not a count that
147 // an arrival branch could compare against a capacity.
148 if (nd.nodetype == NodeType::Source) return std::make_pair(zero, nir);
149
150 std::vector<std::size_t> K(R, 1), Ks(R, 0);
151 std::size_t srvw = 0;
152 for (std::size_t r = 0; r < R; ++r) {
153 K[r] = sn.phasessz_of(ist, r + 1);
154 Ks[r] = srvw;
155 srvw += K[r];
156 }
157 if (state_i.size() < nvar + srvw)
158 throw InputError("to_marginal_aggr: state row is narrower than its server block");
159 const std::size_t srv0 = state_i.size() - nvar - srvw;
160
161 for (std::size_t r = 0; r < R; ++r)
162 for (std::size_t k = 0; k < K[r]; ++k) nir[r] += state_i[srv0 + Ks[r] + k];
163
164 const SchedStrategy sched = sn.stations[ist - 1].sched;
165 if (sched == SchedStrategy::EXT) {
166 // Reached only by an EXT station that is not a Source node, since the
167 // Source returns zero above. It carries the same clamp as `to_marginal`
168 // and for the same reason: an exact type has no infinity, and building
169 // one from a double Inf throws rather than saturating.
170 const T ext = num_traits<T>::is_exact
173 std::numeric_limits<double>::infinity());
174 for (std::size_t r = 0; r < R; ++r) nir[r] = ext;
175 } else if (state_detail::buffer_is_class_tag(sched)) {
176 for (std::size_t r = 0; r < R; ++r) {
177 const T tag = num_traits<T>::from_int(static_cast<long>(r + 1));
178 for (std::size_t b = 0; b < srv0; ++b)
179 if (state_i[b] == tag) nir[r] += num_traits<T>::from_int(1);
180 }
181 } else if (state_detail::buffer_is_tag_phase_pairs(sched)) {
182 // Only the EVEN positions are class tags; the odd ones record the phase
183 // each preempted job was interrupted in. Without this arm a preempted
184 // job was invisible here and nir counted the server alone, unlike
185 // `to_marginal`, which has carried the paired decode all along.
186 if (srv0 > 1)
187 for (std::size_t r = 0; r < R; ++r) {
188 const T tag = num_traits<T>::from_int(static_cast<long>(r + 1));
189 for (std::size_t b = 0; b < srv0; b += 2)
190 if (state_i[b] == tag) nir[r] += num_traits<T>::from_int(1);
191 }
192 } else if (state_detail::buffer_is_per_class_count(sched)) {
193 for (std::size_t r = 0; r < R && r < srv0; ++r) nir[r] += state_i[r];
194 } else if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI) {
195 // A PAS / OI row is the ORDERED JOB LIST and nothing else: entry `b` is
196 // the 1-based class of the job in position b, 0 for an empty slot, which
197 // is the encoding `after_event_station_pas` reads and writes. It has no
198 // buffer/server split at all, so the server-block sum taken above
199 // counted the LAST LIST POSITION as a phase occupancy -- it is discarded
200 // here and the whole row is scanned instead.
201 //
202 // WITHOUT THIS BRANCH the station reports a queue length of about zero
203 // while the jobs are demonstrably in it, in EVERY solver that reduces a
204 // state through this function (SolverCTMC's `solver_ctmc_avg_from_pi`
205 // and SolverSSA's serial analyzer both do), and the capacity filter in
206 // `after_event_station_arv` compares that zero against the station's
207 // bound. A wrong NUMBER, never an error.
208 for (std::size_t r = 0; r < R; ++r) nir[r] = zero;
209 const std::size_t w = state_i.size() > nvar ? state_i.size() - nvar : 0;
210 for (std::size_t b = 0; b < w; ++b) {
211 const double v = num_traits<T>::to_double(state_i[b]);
212 const long tag = static_cast<long>(v + 0.5);
213 if (tag >= 1 && static_cast<std::size_t>(tag) <= R)
214 nir[tag - 1] += num_traits<T>::from_int(1);
215 }
216 }
217
218 // A disabled class holds no jobs whatever the row says. A Place is exempt:
219 // its tokens are not services, so it has no rate to be disabled.
220 if (nd.nodetype != NodeType::Place)
221 for (std::size_t r = 0; r < R; ++r)
222 if (sn.disabled[ist - 1][r]) nir[r] = zero;
223
224 T ni = zero;
225 for (std::size_t r = 0; r < R; ++r) ni += nir[r];
226 return std::make_pair(ni, nir);
227}
228
229/**
230 * Port of `State.isPhysicalCapacity`: true when the bound at (ist, class) is a
231 * PHYSICAL capacity rather than a state-space CUTOFF on an open class.
232 *
233 * The distinction decides what a refused arrival means. The producer's
234 * capacity arguments have the cutoff folded in -- `solver_ssa` overwrites
235 * cap/classcap with min(cutoff, physical) -- so at a cutoff boundary they read
236 * finite even with no physical cap. Treating that as physical would turn a
237 * state-space TRUNCATION into a self-loop loss, which reports a wrong arrival
238 * rate and perturbs the sample path.
239 *
240 * The in-producer signal is the DROP RULE: `refreshCapacity` sets a non-WAITQ
241 * rule exactly when the capacity is physical, and a cutoff-bounded open class
242 * keeps the WAITQ default.
243 */
244template <class T>
245bool is_physical_capacity(const NetworkStruct<T>& sn, std::size_t ist, std::size_t cls) {
246 if (sn.droprule.size() < ist || sn.droprule[ist - 1].size() < cls) return false;
247 const DropStrategy dr = sn.droprule[ist - 1][cls - 1];
248 return dr != DropStrategy::WAITQ && static_cast<int>(dr) != 0;
249}
250
251/**
252 * Port of `State.arrivalIsLost`: true when an arrival that finds no room is
253 * LOST, false when it must BLOCK the upstream instead. Every refusal path
254 * branches on this, and the two outcomes are encoded differently:
255 *
256 * LOST -> leave the state UNCHANGED, a self-loop. The event still fires,
257 * so the OFFERED job reaches the arrival-rate statistic and the
258 * loss appears as ArvR - Tput. A self-loop cancels on the
259 * generator diagonal, so it cannot move the stationary law.
260 * BLOCKED -> return NO rows. That disables the upstream departure until room
261 * frees, which is what the become-blocked edge tests for.
262 *
263 * The rule is the CLASS TYPE, not the drop rule: a closed network's population
264 * is a defining invariant, so a closed job can never be dropped. An explicit
265 * BAS/BBS/RSRD rule asks for blocking for any class.
266 */
267template <class T>
268bool arrival_is_lost(const NetworkStruct<T>& sn, std::size_t ist, std::size_t cls) {
269 if (sn.droprule.size() >= ist && sn.droprule[ist - 1].size() >= cls) {
270 const DropStrategy dr = sn.droprule[ist - 1][cls - 1];
271 if (dr == DropStrategy::BAS || dr == DropStrategy::BBS || dr == DropStrategy::RSRD)
272 return false; // the user asked for blocking explicitly
273 }
274 // The rule above sees only THIS station's declaration. Under the upstream
275 // declaration form the BAS rule sits on the blocking station, not on the
276 // destination where the refusal happens, so the destination side is recorded
277 // separately by `refresh_bas_blocking`. Without this branch an open class
278 // refused here would be declared lost, the become-blocked edge would never
279 // fire, and the blocking station would behave as if its destination were
280 // unbounded.
281 if (sn.isbasdestination.size() >= ist && sn.isbasdestination[ist - 1].size() >= cls &&
282 sn.isbasdestination[ist - 1][cls - 1])
283 return false;
284 // Open -> lost, closed -> blocked. This decides only what happens once the
285 // arrival has already been refused, never whether it is refused.
286 const double nj = sn.njobs()[cls - 1];
287 return !std::isfinite(nj);
288}
289
290/** How a station's state row splits into [buffer | server | local vars]. */
291template <class T>
292struct RowLayout {
293 std::vector<std::size_t> K; ///< phases per class
294 std::vector<std::size_t> Ks; ///< offset of class r's phase block
295 std::size_t srvw = 0; ///< total server width
296 std::size_t nvar = 0; ///< local-variable width
297 std::size_t bufw = 0; ///< buffer width, the only discipline-dependent part
298};
299
300template <class T>
301RowLayout<T> row_layout(const NetworkStruct<T>& sn, std::size_t ind, std::size_t width) {
302 const std::size_t R = sn.nclasses;
303 const std::size_t ist = sn.nodes[ind - 1].station;
304 RowLayout<T> L;
305 L.K.assign(R, 1);
306 L.Ks.assign(R, 0);
307 for (std::size_t r = 0; r < R; ++r) {
308 L.K[r] = sn.phasessz_of(ist, r + 1);
309 L.Ks[r] = L.srvw;
310 L.srvw += L.K[r];
311 }
312 L.nvar = sn.nvars_of(ind);
313 if (width < L.nvar + L.srvw)
314 throw InputError("after_event_station: the state row is narrower than its server block");
315 L.bufw = width - L.nvar - L.srvw;
316 return L;
317}
318
319/**
320 * The entry-phase distribution `pie{ist}{class}`: which phase a service STARTS
321 * in. This is `map_pie`, the equilibrium embedded at DEPARTURE instants, and
322 * NOT `map_prob`, the time-stationary law of D0+D1 -- the two differ whenever
323 * the process is not exponential (for Erlang-2, entry is [1,0] while the
324 * time-stationary law is [0.5,0.5]).
325 *
326 * A Place has no service process, so its "phases" carry no rate; the reference
327 * falls back on a uniform choice there rather than leaving the vector NaN.
328 */
329template <class T>
330std::vector<T> entry_phase_dist(const NetworkStruct<T>& sn, std::size_t ist, std::size_t cls) {
331 const std::size_t nph = sn.phases_of(ist, cls);
332 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
333 std::vector<T> pie;
334 if (d.D0.rows() == nph && d.D1.rows() == nph && nph > 0 && !d.disabled) {
335 mam::Map<T> m;
336 m.D0 = d.D0;
337 m.D1 = d.D1;
338 try {
339 pie = mam::map_pie(m);
340 } catch (const Error&) {
341 pie.clear(); // a zero-rate process has no entry law; fall back below
342 }
343 }
344 bool ok = pie.size() == nph;
345 if (ok) {
347 for (std::size_t k = 0; k < nph; ++k) s += pie[k];
348 ok = num_traits<T>::to_double(s) > 0 && std::isfinite(num_traits<T>::to_double(s));
349 }
350 if (!ok) {
351 pie.assign(nph, num_traits<T>::from_int(0));
352 if (nph > 0) {
354 static_cast<long>(nph)));
355 for (std::size_t k = 0; k < nph; ++k) pie[k] = u;
356 }
357 }
358 return pie;
359}
360
361/** Where node `ind` keeps its reply-block counters inside the local vars. */
363 std::vector<std::size_t> classes; ///< 1-based calling classes holding a block
364 std::vector<std::size_t> slot; ///< slot[r-1] = 0-based column, or npos
365 std::size_t width = 0;
366};
367
368/** Defined below; the polling branches of ARV, DEP and SWITCH use these. */
369template <class T>
370void polling_get(const PollingInfo<T>& pi, const std::vector<T>& var, std::size_t srvclass,
371 std::size_t& pos, std::size_t& swk, long& ctr);
372template <class T>
373std::vector<T> polling_set(const PollingInfo<T>& pi, std::vector<T> var, std::size_t pos,
374 std::size_t swk, long ctr);
375template <class T>
376void polling_next(const PollingInfo<T>& pi, std::size_t pos, const std::vector<long>& nbuf,
377 std::size_t R, bool arrived, std::size_t& q, int& mode, long& budget);
378template <class T>
379void polling_land(const NetworkStruct<T>& sn, std::size_t ist, const PollingInfo<T>& pi,
380 std::size_t q, int mode, long budget, const std::vector<T>& buf,
381 const std::vector<T>& srv, const std::vector<T>& var, const RowLayout<T>& L,
382 std::vector<std::vector<T>>& rows, std::vector<T>& probs);
383
384/** Defined below; the reply block subtracts held servers in the ARV branch. */
385template <class T>
386double reply_blocked(const NetworkStruct<T>& sn, std::size_t ind, const std::vector<T>& var);
387
388/** Defined below; the departure branch records a server held for a reply. */
389template <class T>
390ReplyBlockInfo reply_block_info(const NetworkStruct<T>& sn, std::size_t ind);
391
392/** Defined below; the ARV and DEP branches divert to it before any slicing. */
393template <class T>
395 const std::vector<T>& inspace, EventType event,
396 std::size_t cls);
397
398/**
399 * Port of the ARV branch of `State.afterEventStation`: an arriving class-`cls`
400 * job joins node `ind`, whose local state is `inspace`.
401 *
402 * The event is PASSIVE -- the upstream departure sets the rate -- so every row
403 * returned carries the -1 sentinel. It is nevertheless the branch with the most
404 * successors, because the entering job chooses its service phase, and under the
405 * preemptive disciplines it also chooses which job to displace.
406 *
407 * ONE ROW IN, MANY ROWS OUT. The reference threads a whole matrix of input
408 * rows through this handler and partitions them with logical masks
409 * (`idle_srv`, `all_busy_srv`). Every caller in the CTMC and SSA paths passes a
410 * single row, so those masks degenerate to a branch, which is what this port
411 * writes. The successor set is identical.
412 */
413template <class T>
415 const std::vector<T>& inspace, std::size_t cls) {
416 const std::size_t R = sn.nclasses;
417 const std::size_t ist = sn.nodes[ind - 1].station;
418 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
419 const T minus_one = num_traits<T>::from_int(-1);
420 EventOutcome<T> out;
421 if (ist == 0) throw InputError("after_event_station_arv: node is not a station");
422 const SchedStrategy sched = sn.stations[ist - 1].sched;
423 // A pass-and-swap station has no buffer/server split at all, so it must be
424 // diverted before any slicing happens.
425 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI)
426 return after_event_station_pas(sn, ind, inspace, EventType::ARV, cls);
427 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
428
429 // A Place holds a marking, not a service facility: it has no servers and no
430 // phases, so an arriving token only increments the class marking. Running
431 // it through the scheduling branches below would write into a phase slot
432 // the row does not have, widening the state so it no longer matches the
433 // enumerated space -- and the arrival would then be silently dropped.
434 if (sn.nodes[ind - 1].nodetype == NodeType::Place) {
435 std::vector<T> row = inspace;
436 const double cap = sn.classcap[ist - 1][cls - 1];
437 if (num_traits<T>::to_double(inspace[cls - 1]) < cap) {
438 row[cls - 1] += one;
439 out.space.push_back(row);
440 out.rate.push_back(minus_one);
441 out.prob.push_back(one);
442 } else {
443 // Place full: the arrival is blocked and lost, with no state change.
444 out.space.push_back(row);
445 out.rate.push_back(minus_one);
446 out.prob.push_back(zero);
447 }
448 return out;
449 }
450
451 const std::pair<T, std::vector<T>> mg = to_marginal_aggr(sn, ind, inspace);
452 const double ni = num_traits<T>::to_double(mg.first);
453 const double nir_c = num_traits<T>::to_double(mg.second[cls - 1]);
454 const double cap_i = sn.cap[ist - 1];
455 const double ccap = sn.classcap[ist - 1][cls - 1];
456 const double S = sn.stations[ist - 1].nservers;
457 const std::vector<T> pentry = entry_phase_dist(sn, ist, cls);
458
459 for (std::size_t kentry = 0; kentry < L.K[cls - 1]; ++kentry) {
460 std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
461 std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
462 std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
463 std::vector<std::vector<T>> cand; // (buf, srv, var) triples, flattened below
464 std::vector<T> cand_prob;
465 // START/PREEMPT tag of each candidate: the class that takes a server on
466 // that row and the class it displaces, 0 for neither. Filtered with the
467 // rows themselves at the capacity gate below.
468 std::vector<std::size_t> cand_start, cand_preempt;
469
470 double occ = 0; // jobs currently in service
471 for (std::size_t j = 0; j < srv.size(); ++j) occ += num_traits<T>::to_double(srv[j]);
472
473 if (sched == SchedStrategy::EXT) {
474 // A Source accepts a virtual arrival from the Sink for any open
475 // class: the reservoir is unbounded, so the state does not move.
476 if (!std::isfinite(sn.njobs()[cls - 1])) {
477 out.space.push_back(inspace);
478 out.rate.push_back(zero);
479 out.prob.push_back(one);
480 return out;
481 }
482 continue;
483 }
484
485 if (sched == SchedStrategy::PS || sched == SchedStrategy::INF ||
486 sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
487 sched == SchedStrategy::PSPRIO || sched == SchedStrategy::DPSPRIO ||
488 sched == SchedStrategy::GPSPRIO || sched == SchedStrategy::LPS) {
489 // Every job is in service at once, so the arrival never queues.
490 const std::size_t col = L.Ks[cls - 1] + kentry;
491 std::size_t started = 0;
492 if (num_traits<T>::to_double(srv[col]) < ccap) {
493 srv[col] += one;
494 started = cls; // the job enters service at once
495 cand_prob.push_back(pentry[kentry]);
496 } else {
497 cand_prob.push_back(zero);
498 }
499 std::vector<T> row = buf;
500 row.insert(row.end(), srv.begin(), srv.end());
501 row.insert(row.end(), var.begin(), var.end());
502 cand.push_back(row);
503 cand_start.push_back(started);
504 cand_preempt.push_back(0);
505 } else if (sched == SchedStrategy::POLLING) {
506 // The CONTROLLER decides who is served, not the arrival: a job
507 // joins its class buffer and waits for the server to walk to it,
508 // even when the facility is idle, because the server is then in a
509 // switchover. The one exception is a PARKED server, which only
510 // arises with an empty station and immediate switchovers: it
511 // reaches the arriving job in zero time and opens a visit at once.
512 const PollingInfo<T> pinfo = polling_info(sn, ind);
513 std::size_t srvclass = 0;
514 for (std::size_t r = 1; r <= R; ++r) {
515 double tot = 0;
516 for (std::size_t p = 0; p < L.K[r - 1]; ++p)
517 tot += num_traits<T>::to_double(srv[L.Ks[r - 1] + p]);
518 if (tot > 0) { srvclass = r; break; }
519 }
520 std::size_t pos = 0, swk = 0;
521 long ctr = 0;
522 polling_get(pinfo, var, srvclass, pos, swk, ctr);
523 if (srvclass == 0 && swk == 0) {
524 std::vector<long> nbuf(R, 0);
525 for (std::size_t r = 0; r < R && r < L.bufw; ++r)
526 nbuf[r] = static_cast<long>(num_traits<T>::to_double(buf[r]));
527 nbuf[cls - 1] += 1;
528 std::size_t q = 0;
529 int mode = 0;
530 long budget = 0;
531 polling_next(pinfo, pos, nbuf, R, true, q, mode, budget);
532 srv[L.Ks[cls - 1] + kentry] += one;
533 var = polling_set(pinfo, var, q, 0, budget);
534 cand_start.push_back(cls); // a parked server takes it at once
535 } else {
536 buf[cls - 1] += one;
537 cand_start.push_back(0);
538 }
539 cand_preempt.push_back(0);
540 cand_prob.push_back(pentry[kentry]);
541 std::vector<T> row = buf;
542 row.insert(row.end(), srv.begin(), srv.end());
543 row.insert(row.end(), var.begin(), var.end());
544 cand.push_back(row);
545 } else if (sched == SchedStrategy::SIRO || sched == SchedStrategy::SEPT ||
546 sched == SchedStrategy::LEPT) {
547 // Test the SERVER occupancy, not the total count: the two agree in
548 // work-conserving states, but an immediate-feedback self-loop
549 // transiently leaves an idle server with a non-empty buffer, and
550 // the fed-back job must re-enter the vacated server.
551 if (occ < S) {
552 srv[L.Ks[cls - 1] + kentry] += one;
553 cand_start.push_back(cls);
554 } else {
555 buf[cls - 1] += one;
556 cand_start.push_back(0);
557 }
558 cand_preempt.push_back(0);
559 cand_prob.push_back(pentry[kentry]);
560 std::vector<T> row = buf;
561 row.insert(row.end(), srv.begin(), srv.end());
562 row.insert(row.end(), var.begin(), var.end());
563 cand.push_back(row);
564 } else if (state_detail::buffer_is_class_tag(sched)) {
565 // ORDERED BUFFER. An idle server takes the job; otherwise it goes
566 // to the first empty slot, and if there is none the arrival is
567 // refused -- which is a LOSS or a BLOCK, never a silent drop.
568 // Servers held for a pending REPLY are NOT available to an
569 // arriving job, so a job may already have to wait while the raw
570 // occupancy is below the server count. Zero for every model
571 // without reply signals.
572 const double seff = S - reply_blocked(sn, ind, var);
573 if (occ < seff) {
574 srv[L.Ks[cls - 1] + kentry] += one;
575 std::vector<T> row = buf;
576 row.insert(row.end(), srv.begin(), srv.end());
577 row.insert(row.end(), var.begin(), var.end());
578 cand.push_back(row);
579 cand_prob.push_back(pentry[kentry]);
580 cand_start.push_back(cls);
581 cand_preempt.push_back(0);
582 } else {
583 std::size_t slot = 0; // 1-based index of the LAST empty slot
584 for (std::size_t b = 0; b < L.bufw; ++b)
585 if (num_traits<T>::to_double(buf[b]) == 0) slot = b + 1;
586 // A structurally free column is not enough: the CAPACITY must
587 // also permit the placement. Gating on the capacity only for a
588 // PHYSICAL bound keeps a state-space cutoff a truncation --
589 // firing the gate at a cutoff would turn it into a self-loop.
590 const bool has_room = is_physical_capacity(sn, ist, cls)
591 ? (ni < cap_i && nir_c < ccap)
592 : true;
593 if (slot > 0 && has_room) {
594 buf[slot - 1] = num_traits<T>::from_int(static_cast<long>(cls));
595 std::vector<T> row = buf;
596 row.insert(row.end(), srv.begin(), srv.end());
597 row.insert(row.end(), var.begin(), var.end());
598 cand.push_back(row);
599 cand_prob.push_back(pentry[kentry]);
600 cand_start.push_back(0); // the job waits in the buffer
601 cand_preempt.push_back(0);
602 } else if (arrival_is_lost(sn, ist, cls) &&
603 is_physical_capacity(sn, ist, cls)) {
604 // LOST: keep the row unchanged, so the event still fires
605 // and the OFFERED job reaches the arrival-rate statistic.
606 // A self-loop cancels on the generator diagonal, so the
607 // stationary law cannot move.
608 //
609 // ONLY A DECLARED BUFFER MAY LOSE A JOB. `slot == 0` also
610 // fires when the row is merely as wide as the STATE-SPACE
611 // CUTOFF let it be, and there the reference emits no
612 // successor at all -- the state above the cutoff is absent,
613 // not refused. Emitting the self-loop there costs nothing on
614 // the generator (the diagonal absorbs it) and everything on
615 // the RATES, which count the loop as a departure of the
616 // upstream Source: on mqn_multiserver_fcfs 20 such loops
617 // moved Source Tput from 0.24763 to 0.26040, an arrival rate
618 // no job ever carried, and left ArvR above Tput at a station
619 // that drops nothing. `has_room` above already draws exactly
620 // this line for the capacity test.
621 cand.push_back(inspace);
622 cand_prob.push_back(pentry[kentry]);
623 cand_start.push_back(0); // the job is lost: it starts nothing
624 cand_preempt.push_back(0);
625 }
626 // BLOCKED: emit nothing, which disables the upstream departure
627 // until room frees. That absence is what the become-blocked
628 // edge tests for.
629 }
630 } else if (state_detail::buffer_is_tag_phase_pairs(sched)) {
631 // PREEMPTIVE FAMILY. The buffer holds [class, phase] pairs, because
632 // a displaced job must remember where it was interrupted. An idle
633 // server simply takes the arrival; a busy one forces a CHOICE of
634 // victim, so the event has one successor per (class, phase) in
635 // service, weighted by that server's share of the occupancy.
636 if (occ < S) {
637 srv[L.Ks[cls - 1] + kentry] += one;
638 std::vector<T> row = buf;
639 row.insert(row.end(), srv.begin(), srv.end());
640 row.insert(row.end(), var.begin(), var.end());
641 cand.push_back(row);
642 cand_prob.push_back(pentry[kentry]);
643 cand_start.push_back(cls);
644 cand_preempt.push_back(0);
645 } else {
646 // Priority-awareness is a property of the DECLARED policy, never
647 // of the data: inferring it from the class priorities turned
648 // plain LCFSPR/FCFSPR into something that is neither the base
649 // policy nor the PRIO variant.
650 const bool prio_aware = sched == SchedStrategy::FCFSPRPRIO ||
651 sched == SchedStrategy::FCFSPIPRIO ||
652 sched == SchedStrategy::LCFSPRPRIO ||
653 sched == SchedStrategy::LCFSPIPRIO;
654 const bool lcfs_family = sched == SchedStrategy::LCFSPRPRIO ||
655 sched == SchedStrategy::LCFSPIPRIO;
656 // FCFS-PR/PI: an ARRIVAL NEVER PREEMPTS. The base discipline
657 // serves in arrival order, and the PLAIN variants read no
658 // priorities at all, so every class sits in ONE group and the
659 // arrival can only wait; preemption belongs to the PRIO variants
660 // and only ACROSS groups. LCFS-PR/PI is the opposite case, keeping
661 // the NEWEST job in service, so there a plain arrival always
662 // preempts. Letting plain FCFSPR/FCFSPI preempt made them LCFS-PR
663 // by another name: invisible on an exponential single-chain queue,
664 // whose queue-length process is the same either way, and wrong
665 // wherever WHICH job holds the server matters -- a fork-join, where
666 // the sibling that completes decides when the Join fires, or any
667 // phase-type service, where the interrupted phase is carried.
668 const bool fcfs_never_preempts = sched == SchedStrategy::FCFSPR ||
669 sched == SchedStrategy::FCFSPI;
670 // PR resumes the victim in the phase it held; PI restarts it
671 // from the entry phase. That single value is the whole
672 // difference between the two families in this branch.
673 const bool resume = sched == SchedStrategy::LCFSPR ||
674 sched == SchedStrategy::LCFSPRPRIO ||
675 sched == SchedStrategy::FCFSPR ||
676 sched == SchedStrategy::FCFSPRPRIO;
677 bool can_preempt_any = false;
678 for (std::size_t cp = 1; cp <= R && !fcfs_never_preempts; ++cp) {
679 if (prio_aware) {
680 // Across priority groups a strictly higher-priority
681 // arrival preempts. WITHIN a group the base discipline
682 // decides: LCFS-PR keeps the NEWEST job in service, so
683 // an equal-priority arrival preempts; FCFS-PR never
684 // lets an arrival preempt.
685 const int pa = sn.classes[cls - 1].prio;
686 const int pv = sn.classes[cp - 1].prio;
687 if (lcfs_family ? (pa > pv) : (pa >= pv)) continue;
688 }
689 for (std::size_t pp = 0; pp < L.K[cp - 1]; ++pp) {
690 const std::size_t vcol = L.Ks[cp - 1] + pp;
691 const double busy = num_traits<T>::to_double(srv[vcol]);
692 if (busy <= 0) continue;
693 can_preempt_any = true;
694 std::vector<T> b2 = buf, s2 = srv;
695 s2[vcol] -= one;
696 s2[L.Ks[cls - 1] + kentry] += one;
697 // Rightmost empty PAIR, which is where a displaced job
698 // is stored; the class column is one left of the zero
699 // the scan finds.
700 std::size_t slot = 0;
701 for (std::size_t b = 0; b < L.bufw; ++b)
702 if (num_traits<T>::to_double(b2[b]) == 0) slot = b;
703 if (slot == 0) continue; // no room to hold the victim
704 b2[slot - 1] = num_traits<T>::from_int(static_cast<long>(cp));
705 b2[slot] = resume ? num_traits<T>::from_int(static_cast<long>(pp + 1))
706 : one;
707 std::vector<T> row = b2;
708 row.insert(row.end(), s2.begin(), s2.end());
709 row.insert(row.end(), var.begin(), var.end());
710 cand.push_back(row);
711 // the displaced job leaves the server and the arriving
712 // one takes it, on the same arc
713 cand_start.push_back(cls);
714 cand_preempt.push_back(cp);
715 // The victim is drawn uniformly among the jobs in
716 // service, so its share of the occupancy weights the
717 // successor.
718 cand_prob.push_back(T(pentry[kentry] * num_traits<T>::from_double(busy / occ)));
719 }
720 }
721 // Every busy server holds a job this arrival may not displace,
722 // so the job WAITS instead. Without this the loop above emits
723 // nothing, the arrival transition does not exist at all, and
724 // the class can never enter a busy station -- its queue is
725 // then silently understated. It is stored as a (class, entry
726 // phase) pair exactly as a preempted job is, so promotion
727 // resumes it from that phase.
728 if ((prio_aware || fcfs_never_preempts) && !can_preempt_any) {
729 std::size_t slot = 0;
730 for (std::size_t b = 0; b < L.bufw; ++b)
731 if (num_traits<T>::to_double(buf[b]) == 0) slot = b;
732 if (slot > 0) {
733 std::vector<T> b2 = buf;
734 b2[slot - 1] = num_traits<T>::from_int(static_cast<long>(cls));
735 b2[slot] = num_traits<T>::from_int(static_cast<long>(kentry + 1));
736 std::vector<T> row = b2;
737 row.insert(row.end(), srv.begin(), srv.end());
738 row.insert(row.end(), var.begin(), var.end());
739 cand.push_back(row);
740 cand_prob.push_back(pentry[kentry]);
741 cand_start.push_back(0); // it preempts nothing and waits
742 cand_preempt.push_back(0);
743 }
744 }
745 }
746 } else {
747 throw UnsupportedError(
748 std::string("after_event_station_arv: the ") + lang::sched_to_text(sched) +
749 " discipline is not ported yet");
750 }
751
752 // The capacity filter of the reference: drop any successor that would
753 // exceed the station or class bound.
754 for (std::size_t c = 0; c < cand.size(); ++c) {
755 const std::pair<T, std::vector<T>> og = to_marginal_aggr(sn, ind, cand[c]);
756 if (num_traits<T>::to_double(og.second[cls - 1]) > ccap) continue;
757 if (num_traits<T>::to_double(og.first) > cap_i) continue;
758 out.space.push_back(cand[c]);
759 out.rate.push_back(minus_one);
760 out.prob.push_back(cand_prob[c]);
761 // the capacity gate drops rows, so the tags are attached here
762 tag_last(out, c < cand_start.size() ? cand_start[c] : 0,
763 c < cand_preempt.size() ? cand_preempt[c] : 0);
764 }
765 }
766 pad_tags(out);
767 return out;
768}
769
770/**
771 * `sn.mu` and `sn.phi` for one (station, class), derived as MATLAB's
772 * `Markovian.getMu` / `getPhi` derive them from the (D0, D1) pair:
773 *
774 * mu(k) = -D0(k,k) total exit rate of phase k
775 * phi(k) = sum_j D1(k,j) / -D0(k,k) probability the exit COMPLETES service
776 *
777 * so mu*phi is the departure rate and mu*(1-phi) the phase-advance rate. An
778 * Immediate process has D0(1,1) = 0 and takes phi = 1, since every exit of a
779 * zero-duration service is a completion.
780 */
781template <class T>
782std::pair<std::vector<T>, std::vector<T>> phase_rates(const NetworkStruct<T>& sn,
783 std::size_t ist, std::size_t cls) {
784 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
785 const std::size_t n = sn.phases_of(ist, cls);
786 std::vector<T> mu(n, num_traits<T>::from_int(0)), phi(n, num_traits<T>::from_int(1));
787 if (d.D0.rows() != n || d.D1.rows() != n) return std::make_pair(mu, phi);
788 for (std::size_t k = 0; k < n; ++k) {
789 const T dk = T(-d.D0(k, k));
790 mu[k] = dk;
791 if (num_traits<T>::to_double(dk) == 0) {
792 phi[k] = num_traits<T>::from_int(1); // Immediate: every exit completes
793 continue;
794 }
796 for (std::size_t j = 0; j < n; ++j) s += d.D1(k, j);
797 phi[k] = T(s / dk);
798 }
799 return std::make_pair(mu, phi);
800}
801
802/** The limited-load-dependent multiplier at population `n`, 1 when unset. */
803template <class T>
804T lld_factor(const NetworkStruct<T>& sn, std::size_t ist, double n) {
805 const std::vector<T>& s = sn.stations[ist - 1].lldscaling;
806 if (s.empty()) return num_traits<T>::from_int(1);
807 // Beyond the declared levels the scaling holds at its last value, which is
808 // what "limited" load-dependence means: the curve is flat past the limit.
809 if (!std::isfinite(n) || n >= static_cast<double>(s.size())) return s.back();
810 if (n < 1) return num_traits<T>::from_int(1);
811 return s[static_cast<std::size_t>(n) - 1];
812}
813
814/**
815 * Port of `State.cdclassfactor`: the class-dependence multiplier of a class-`cls`
816 * rate at the per-class population `nir`.
817 *
818 * `cdscaling` maps a 1 x R population vector to the R dimensionless scalings
819 * beta_r(n); the component of the class whose service is firing is the factor.
820 * A handle returning a SCALAR is the neutral case and yields 1 for every class,
821 * which is why the read is clamped to the vector's last entry rather than
822 * indexed blindly -- the reference's `v(min(class, numel(v)))`.
823 *
824 * `jdscaling` is folded in multiplicatively here, exactly as
825 * `State.afterEventInit` folds eta_i into the effective per-station handle.
826 * Doing it at the point of use rather than by rewriting the struct keeps the two
827 * fields distinguishable for the product-form tests elsewhere.
828 */
829template <class T>
830T cd_factor(const NetworkStruct<T>& sn, std::size_t ist, const std::vector<T>& nir,
831 std::size_t cls) {
832 const T one = num_traits<T>::from_int(1);
833 const Station<T>& st = sn.stations[ist - 1];
834 if (!st.cdscaling && !st.jdscaling) return one;
835 T f = one;
836 const CdScaling<T>* handles[2] = {&st.cdscaling, &st.jdscaling};
837 for (std::size_t h = 0; h < 2; ++h) {
838 const CdScaling<T>& fun = *handles[h];
839 if (!fun) continue;
840 const std::vector<T> v = fun(nir);
841 if (v.empty())
842 throw InputError(
843 "cd_factor: the class-dependence map returned an empty scaling vector");
844 f = T(f * v[std::min(cls, v.size()) - 1]);
845 }
846 return f;
847}
848
849/**
850 * The population the *PRIO disciplines actually share the server among.
851 *
852 * While every job fits in a server nobody is waiting and precedence is moot, so
853 * this is the plain marginal. Once the station saturates only the MOST URGENT
854 * group present is served (a LOWER `classprio` value is more urgent in LINE),
855 * and both the share and the load-dependent lookup are taken over that group
856 * alone -- `nirprio` / `niprio` in `State.afterEventStation`.
857 */
858template <class T>
859struct PrioPop {
860 std::vector<T> nir; ///< `nirprio` when masked, the plain marginal otherwise
861 double ni = 0; ///< `niprio` when masked, the plain total otherwise
862 bool masked = false; ///< whether the saturated-station mask was applied
863 bool served = true; ///< false when `cls` is not in the most urgent group
864};
865
866/** Compute the *PRIO effective population; a no-op for every other discipline. */
867template <class T>
868PrioPop<T> prio_pop(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
869 std::size_t cls, double ni, double S) {
870 const std::size_t R = sn.nclasses;
871 const SchedStrategy sched = sn.stations[ist - 1].sched;
872 PrioPop<T> p;
873 p.nir = m.nir;
874 p.ni = ni;
875 const bool prio_aware = sched == SchedStrategy::PSPRIO ||
876 sched == SchedStrategy::DPSPRIO ||
877 sched == SchedStrategy::GPSPRIO;
878 if (!prio_aware || ni <= S) return p;
879 double best = std::numeric_limits<double>::infinity();
880 for (std::size_t r = 0; r < R; ++r)
881 if (num_traits<T>::to_double(m.nir[r]) > 0)
882 best = std::min(best, static_cast<double>(sn.classes[r].prio));
883 if (static_cast<double>(sn.classes[cls - 1].prio) != best) {
884 p.served = false;
885 return p;
886 }
887 p.masked = true;
888 p.ni = 0;
889 for (std::size_t r = 0; r < R; ++r) {
890 if (static_cast<double>(sn.classes[r].prio) != best)
893 }
894 return p;
895}
896
897/** Defined below; DEP and PHASE must share one definition of the share. */
898template <class T>
899T service_share(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
900 std::size_t cls, double ni, double S);
901
902/**
903 * Port of the DEP branch of `State.afterEventStation`: a class-`cls` job
904 * completes service at station `ind`.
905 *
906 * This is the ACTIVE half, so unlike ARV it carries real rates, and the rate is
907 * where the disciplines actually differ -- the state update is nearly the same
908 * for all of them. Two rate conventions appear, and they are not
909 * interchangeable:
910 *
911 * mu(k)*phi(k)*kir -- the processor-sharing family, where the completion
912 * rate is the phase rate times the share of the server
913 * D1(k,kdest)*kir -- the FCFS family, where the MAP matrix already encodes
914 * both the completion and the phase the NEXT service
915 * starts in, so the destination phase is enumerated
916 */
917template <class T>
919 const std::vector<T>& inspace, std::size_t cls,
920 bool no_promote = false) {
921 const std::size_t R = sn.nclasses;
922 const std::size_t ist = sn.nodes[ind - 1].station;
923 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
924 EventOutcome<T> out;
925 if (ist == 0) throw InputError("after_event_station_dep: node is not a station");
926 const SchedStrategy sched = sn.stations[ist - 1].sched;
927 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI)
928 return after_event_station_pas(sn, ind, inspace, EventType::DEP, cls);
929 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
930 const double S = sn.stations[ist - 1].nservers;
931
932 std::vector<std::size_t> ph(R, 1), shift(R, 0);
933 for (std::size_t r = 0; r < R; ++r) {
934 ph[r] = L.K[r];
935 shift[r] = L.Ks[r];
936 }
937 // THE MARK OF A MARKED (MMAP) SOURCE CLASS, and the class whose phase block
938 // its departures actually read. All marks share ONE modulating chain, held
939 // in the CARRIER's block (mark 1); a later mark owns a single always-zero
940 // column (`phasessz_of`), so reading its own block would find no token and
941 // the class would never emit. `markof == 0` leaves every other station and
942 // every unmarked class on `cls`, exactly as before.
943 const std::size_t markof = sched == SchedStrategy::EXT ? sn.markidx_of(ist, cls) : 0;
944 std::size_t phcls = cls;
945 if (markof > 0) {
946 const std::size_t carrier = sn.mark_carrier_of(ist);
947 if (carrier >= 1 && carrier <= R) phcls = carrier;
948 }
949
950 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
951 if (num_traits<T>::to_double(m.sir[phcls - 1]) <= 0) return out; // nothing to depart
952
953 const std::pair<std::vector<T>, std::vector<T>> mp = phase_rates(sn, ist, cls);
954 const std::vector<T>& mu = mp.first;
955 const std::vector<T>& phi = mp.second;
956 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
957
958 double ni = 0;
959 for (std::size_t r = 0; r < R; ++r) ni += num_traits<T>::to_double(m.nir[r]);
960 // Both dependence factors multiply every rate this branch emits, so they are
961 // folded into ONE scalar named `lld` for the arithmetic below. Their
962 // ARGUMENTS differ at a saturated *PRIO station, and the reference is not
963 // uniform about it: the load-dependent lookup takes the priority-masked
964 // total `niprio` for all three *PRIO disciplines, while the class-dependence
965 // handle takes the masked vector `nirprio` only for DPSPRIO and GPSPRIO and
966 // the plain marginal for PSPRIO (afterEventStation.m:709-711 vs :746-748 and
967 // :808-810). That asymmetry is reproduced rather than smoothed: smoothing it
968 // would move every reported metric on a model that has both.
969 const PrioPop<T> pp = prio_pop(sn, ist, m, cls, ni, S);
970 const bool cd_takes_prio =
971 pp.masked && (sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO);
972 const T lld = T(lld_factor(sn, ist, pp.ni) *
973 cd_factor(sn, ist, cd_takes_prio ? pp.nir : m.nir, cls));
974
975 // A retrial station does NOT promote from the orbit on completion: an
976 // orbiting job re-enters only through a RETRY at the retrial rate. The
977 // same suppression serves an immediate-feedback self-loop, where the
978 // departing job holds the server for its own re-arrival.
979 bool suppress_promote = no_promote;
980 {
981 const typename std::map<std::size_t, RetrialParam<T>>::const_iterator rit =
982 sn.retrialparam.find(ist);
983 if (rit != sn.retrialparam.end())
984 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r)
985 if (!rit->second.retrial_proc[r].disabled) { suppress_promote = true; break; }
986 }
987
988 for (std::size_t k = 0; k < L.K[phcls - 1]; ++k) {
989 std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
990 std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
991 std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
992 const std::size_t col = L.Ks[phcls - 1] + k;
993 if (num_traits<T>::to_double(srv[col]) <= 0) continue;
994 const T kir = m.kir[phcls - 1][k];
995
996 if (sched == SchedStrategy::EXT) {
997 // A Source EMITS an arrival. Its reservoir is unbounded, so no job
998 // count changes; what moves is the MODULATING PHASE, from k to
999 // kentry at rate D1(k, kentry). For an exponential source that is
1000 // the single entry lambda and the state is unchanged, which is why
1001 // the arrival stream is memoryless; for a MAP it is exactly the
1002 // correlation the source is there to produce.
1003 if (!std::isfinite(sn.njobs()[cls - 1])) {
1004 // THE MARK'S OWN BLOCK, not the aggregate. `d.D1` is the sum
1005 // over marks, so reading it for a marked class gives every one
1006 // of them the WHOLE stream: two read classes off one MMAP each
1007 // arrived at the full 2.5/s instead of the 1.9 and 0.6 their
1008 // blocks carry, doubling the offered load and erasing the
1009 // popularity difference the marks exist to express.
1010 const Matrix<T>& D1k =
1011 markof > 0 && markof <= d.Dmark.size() ? d.Dmark[markof - 1] : d.D1;
1012 for (std::size_t ke = 0; ke < L.K[phcls - 1]; ++ke) {
1013 const T arv = D1k(k, ke);
1014 if (num_traits<T>::to_double(arv) <= 0) continue;
1015 std::vector<T> row = inspace;
1016 row[L.bufw + L.Ks[phcls - 1] + k] -= one;
1017 row[L.bufw + L.Ks[phcls - 1] + ke] += one;
1018 out.space.push_back(row);
1019 out.rate.push_back(T(lld * arv));
1020 out.prob.push_back(one);
1021 }
1022 }
1023 } else if (sched == SchedStrategy::INF || sched == SchedStrategy::PS ||
1024 sched == SchedStrategy::LPS || sched == SchedStrategy::DPS ||
1025 sched == SchedStrategy::GPS || sched == SchedStrategy::PSPRIO ||
1026 sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO) {
1027 srv[col] -= one;
1028 // The same share PHASE uses: a completion and an internal phase
1029 // advance are driven by the identical fraction of the server, so
1030 // they must never be computed two different ways.
1031 const T rate = T(mu[k] * phi[k] * kir * service_share(sn, ist, m, cls, ni, S));
1032 std::vector<T> row = buf;
1033 row.insert(row.end(), srv.begin(), srv.end());
1034 row.insert(row.end(), var.begin(), var.end());
1035 out.space.push_back(row);
1036 out.rate.push_back(T(lld * rate));
1037 out.prob.push_back(one);
1038 } else if (state_detail::buffer_is_class_tag(sched) && sched != SchedStrategy::FCFS) {
1039 // HOL, LCFS and LCFSPRIO share FCFS's ordered buffer but NOT its
1040 // rate convention: the completion rate is mu*phi*kir, summed over
1041 // destination phases rather than enumerated. What separates the
1042 // three is only WHICH waiting job is promoted.
1043 const bool has_waiting = ni > S && !suppress_promote;
1044 const T rate = T(mu[k] * phi[k] * kir);
1045 srv[col] -= one;
1046 if (!has_waiting) {
1047 std::vector<T> row = buf;
1048 row.insert(row.end(), srv.begin(), srv.end());
1049 row.insert(row.end(), var.begin(), var.end());
1050 out.space.push_back(row);
1051 out.rate.push_back(T(lld * rate));
1052 out.prob.push_back(one);
1053 continue;
1054 }
1055 // Position of the job that starts service, 0-based; L.bufw = none.
1056 std::size_t pos = L.bufw;
1057 if (sched == SchedStrategy::LCFS) {
1058 // Plain LCFS is NOT priority-aware: it always promotes the most
1059 // recent arrival. Arrivals fill the rightmost empty slot, so
1060 // the newest job is the FIRST nonzero column. Branching here on
1061 // the class priorities would silently turn every LCFS station
1062 // with distinct priorities into an LCFSPRIO one.
1063 for (std::size_t b = 0; b < L.bufw; ++b)
1064 if (num_traits<T>::to_double(buf[b]) != 0) { pos = b; break; }
1065 } else {
1066 // HOL and LCFSPRIO serve the highest-priority waiting group
1067 // first; in LINE a LOWER classprio value is more urgent. Within
1068 // the group HOL takes the oldest job (rightmost) and LCFSPRIO
1069 // the newest (leftmost), which is how each relates to its
1070 // non-priority base discipline.
1071 double best = std::numeric_limits<double>::infinity();
1072 for (std::size_t b = 0; b < L.bufw; ++b) {
1073 const double v = num_traits<T>::to_double(buf[b]);
1074 if (v <= 0) continue;
1075 const double p = sn.classes[static_cast<std::size_t>(v) - 1].prio;
1076 if (p < best) best = p;
1077 }
1078 if (std::isfinite(best))
1079 for (std::size_t b = 0; b < L.bufw; ++b) {
1080 const double v = num_traits<T>::to_double(buf[b]);
1081 if (v <= 0) continue;
1082 if (sn.classes[static_cast<std::size_t>(v) - 1].prio != best) continue;
1083 pos = b;
1084 if (sched == SchedStrategy::LCFSPRIO) break; // leftmost
1085 }
1086 }
1087 if (pos == L.bufw) continue;
1088 const std::size_t hc = static_cast<std::size_t>(num_traits<T>::to_double(buf[pos]));
1089 std::vector<T> b2 = buf;
1090 if (sched == SchedStrategy::LCFS) {
1091 // LCFS clears the slot IN PLACE, leaving a hole; the next
1092 // arrival refills it, since arrivals seek the rightmost empty
1093 // slot. The priority variants instead close the gap.
1094 b2[pos] = zero;
1095 } else {
1096 for (std::size_t b = pos; b > 0; --b) b2[b] = b2[b - 1];
1097 b2[0] = zero;
1098 }
1099 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1100 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1101 std::vector<T> s3 = srv;
1102 s3[L.Ks[hc - 1] + ke] += one;
1103 std::vector<T> row = b2;
1104 row.insert(row.end(), s3.begin(), s3.end());
1105 row.insert(row.end(), var.begin(), var.end());
1106 out.space.push_back(row);
1107 out.rate.push_back(T(lld * rate * pentry[ke]));
1108 out.prob.push_back(one);
1109 tag_last(out, hc, 0); // the promoted job takes the freed server
1110 }
1111 } else if (state_detail::buffer_is_class_tag(sched)) {
1112 // FCFS. D1(k, kdest) is the completion rate that leaves the process
1113 // in phase kdest, so the destination phase has to be enumerated
1114 // rather than summed away.
1115 //
1116 // A synchronous call: this departing job KEEPS its server until its
1117 // REPLY returns here, so the server is not handed to a waiting job;
1118 // it is recorded as held in the reply block instead. Servers already
1119 // held that way are likewise unavailable, so a job can be waiting
1120 // while the raw occupancy is below the server count.
1121 std::vector<T> var2 = var;
1122 bool holds_reply = false;
1123 if (sn.replyblock.size() >= ind && sn.replyblock[ind - 1].size() >= cls &&
1124 sn.replyblock[ind - 1][cls - 1]) {
1125 const ReplyBlockInfo ri = reply_block_info(sn, ind);
1126 const std::size_t sl = ri.slot[cls - 1];
1127 if (sl != static_cast<std::size_t>(-1) && sl < var2.size()) {
1128 var2[sl] += one;
1129 holds_reply = true;
1130 }
1131 }
1132 const double nb = reply_blocked(sn, ind, var);
1133 const bool has_waiting = ni > (S - nb) && !suppress_promote && !holds_reply;
1134 for (std::size_t kd = 0; kd < L.K[cls - 1]; ++kd) {
1135 const T rate = T(d.D1(k, kd) * kir);
1136 std::vector<T> s2 = srv;
1137 s2[col] -= one;
1138 if (!has_waiting) {
1139 std::vector<T> row = buf;
1140 row.insert(row.end(), s2.begin(), s2.end());
1141 row.insert(row.end(), var2.begin(), var2.end());
1142 out.space.push_back(row);
1143 out.rate.push_back(T(lld * rate));
1144 out.prob.push_back(one);
1145 continue;
1146 }
1147 // Promote the head of the buffer. The head is the LAST column:
1148 // the buffer shifts RIGHT as jobs join, so the oldest job sits
1149 // at the end -- which is what makes the discipline first-come.
1150 const double headv = num_traits<T>::to_double(buf[L.bufw - 1]);
1151 if (headv <= 0) continue;
1152 const std::size_t hc = static_cast<std::size_t>(headv);
1153 std::vector<T> b2(L.bufw, zero);
1154 for (std::size_t b = 1; b < L.bufw; ++b) b2[b] = buf[b - 1];
1155 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1156 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1157 std::vector<T> s3 = s2;
1158 s3[L.Ks[hc - 1] + ke] += one;
1159 std::vector<T> row = b2;
1160 row.insert(row.end(), s3.begin(), s3.end());
1161 row.insert(row.end(), var2.begin(), var2.end());
1162 const T r3 = T(lld * rate * pentry[ke]);
1163 out.space.push_back(row);
1164 out.rate.push_back(r3);
1165 // A branch that cannot happen carries probability zero, not
1166 // one: a PH whose entry vector does not reach every phase
1167 // would otherwise contribute phantom departures.
1168 out.prob.push_back(num_traits<T>::to_double(r3) == 0 ? zero : one);
1169 tag_last(out, hc, 0); // the head of the buffer takes the freed server
1170 }
1171 }
1172 } else if (sched == SchedStrategy::POLLING) {
1173 // A completion ends the VISIT unless the discipline still allows
1174 // another job of the same class; when it ends, the server walks the
1175 // cyclic order to the next tangible controller state.
1176 const PollingInfo<T> pinfo = polling_info(sn, ind);
1177 const T rate = T(mu[k] * phi[k] * kir);
1178 if (num_traits<T>::to_double(rate) <= 0) continue;
1179 std::size_t pos = 0, swk = 0;
1180 long ctr = 0;
1181 polling_get(pinfo, var, cls, pos, swk, ctr);
1182 // No job can complete while the server is walking.
1183 if (swk != 0) continue;
1184 std::vector<long> nbuf(R, 0);
1185 for (std::size_t r = 0; r < R && r < L.bufw; ++r)
1186 nbuf[r] = static_cast<long>(num_traits<T>::to_double(buf[r]));
1187 srv[col] -= one;
1188 long ctrnext = 0;
1189 bool goon = false;
1190 switch (pinfo.ptype) {
1192 ctrnext = 0;
1193 goon = nbuf[cls - 1] > 0; // the visit ends when it drains
1194 break;
1196 ctrnext = ctr - 1; // one of the gated jobs completed
1197 goon = ctrnext > 0;
1198 break;
1200 ctrnext = ctr - 1; // one of the K permitted services used
1201 goon = ctrnext > 0 && nbuf[cls - 1] > 0;
1202 break;
1204 ctrnext = ctr; // the target level is fixed for the visit
1205 goon = nbuf[cls - 1] > ctr;
1206 break;
1207 }
1208 std::size_t q = cls;
1209 int mode = 1;
1210 long budget = ctrnext;
1211 if (!goon) polling_next(pinfo, cls, nbuf, R, false, q, mode, budget);
1212 std::vector<std::vector<T>> rows;
1213 std::vector<T> probs;
1214 polling_land(sn, ist, pinfo, q, mode, budget, buf, srv, var, L, rows, probs);
1215 for (std::size_t j = 0; j < rows.size(); ++j) {
1216 out.space.push_back(rows[j]);
1217 out.rate.push_back(T(lld * rate * probs[j]));
1218 out.prob.push_back(one);
1219 // mode 1 opens a visit, pulling a waiting class-q job into the
1220 // server; a switchover or a park starts nobody
1221 tag_last(out, mode == 1 ? q : 0, 0);
1222 }
1223 } else if (state_detail::buffer_is_tag_phase_pairs(sched)) {
1224 // THE PREEMPT-RESUME / PREEMPT-INDEPENDENT FAMILY, all eight members.
1225 // Every arm of the reference carries the same rate law, mu*phi*kir
1226 // summed over destination phases (afterEventStation.m:1045, :1081,
1227 // :1118, :1150, :1188, :1234, :1286, :1332); what separates them is
1228 // WHICH waiting job is promoted and in WHICH phase it restarts.
1229 const T rate = T(mu[k] * phi[k] * kir);
1230 srv[col] -= one;
1231 const bool prio_aware = sched == SchedStrategy::FCFSPRPRIO ||
1232 sched == SchedStrategy::FCFSPIPRIO ||
1233 sched == SchedStrategy::LCFSPRPRIO ||
1234 sched == SchedStrategy::LCFSPIPRIO;
1235 const bool lcfs = sched == SchedStrategy::LCFSPR ||
1236 sched == SchedStrategy::LCFSPI ||
1237 sched == SchedStrategy::LCFSPRPRIO ||
1238 sched == SchedStrategy::LCFSPIPRIO;
1239 // PR resumes the promoted job in the phase it was interrupted in;
1240 // PI discards that phase and restarts from the entry distribution.
1241 // That single value is the whole PR-vs-PI difference.
1242 const bool resume = sched == SchedStrategy::LCFSPR ||
1243 sched == SchedStrategy::LCFSPRPRIO ||
1244 sched == SchedStrategy::FCFSPR ||
1245 sched == SchedStrategy::FCFSPRPRIO;
1246 // Class column, 0-based and hence EVEN; L.bufw means "nobody waits".
1247 std::size_t pos = L.bufw;
1248 if (ni > S && !suppress_promote && L.bufw >= 2) {
1249 double best = std::numeric_limits<double>::infinity();
1250 if (prio_aware)
1251 // Only the class columns carry a class tag: reading the
1252 // phase columns too would let a phase index masquerade as a
1253 // class and win the group, which is what the reference's
1254 // FCFSPIPRIO/LCFSPIPRIO arms do (:1290, :1336) while its
1255 // PR-PRIO arms correctly restrict to `class_cols` (:1194).
1256 for (std::size_t b = 0; b + 1 < L.bufw; b += 2) {
1257 const double v = num_traits<T>::to_double(buf[b]);
1258 if (v <= 0) continue;
1259 const double p = sn.classes[static_cast<std::size_t>(v) - 1].prio;
1260 if (p < best) best = p;
1261 }
1262 for (std::size_t b = 0; b + 1 < L.bufw; b += 2) {
1263 const double v = num_traits<T>::to_double(buf[b]);
1264 if (v <= 0) continue;
1265 if (prio_aware && sn.classes[static_cast<std::size_t>(v) - 1].prio != best)
1266 continue;
1267 pos = b;
1268 // The buffer is newest-first, so LCFS takes the FIRST such
1269 // pair (:1052 colfirstnnz) and FCFS the LAST (:1121
1270 // colLastNnz). LCFS does NOT take the rightmost slot.
1271 if (lcfs) break;
1272 }
1273 }
1274 if (pos == L.bufw) {
1275 std::vector<T> row = buf;
1276 row.insert(row.end(), srv.begin(), srv.end());
1277 row.insert(row.end(), var.begin(), var.end());
1278 out.space.push_back(row);
1279 out.rate.push_back(T(lld * rate));
1280 out.prob.push_back(one);
1281 continue;
1282 }
1283 const std::size_t hc = static_cast<std::size_t>(num_traits<T>::to_double(buf[pos]));
1284 const std::size_t kst =
1285 static_cast<std::size_t>(num_traits<T>::to_double(buf[pos + 1]));
1286 if (hc == 0 || hc > R || kst == 0 || kst > L.K[hc - 1])
1287 throw InputError("after_event_station_dep: station '" +
1288 sn.stations[ist - 1].name +
1289 "' holds a waiting job whose [class, phase] pair is malformed");
1290 // Close the hole by padding a whole EMPTY PAIR on the left, keeping
1291 // the buffer right-aligned as `from_marginal` enumerates it and as
1292 // afterEventStation.m:1267-1274 requires. Removing the pair in place
1293 // (:1055, :1125) or padding one slot on each side (:1222) leaves a
1294 // layout the enumerator never emits, so the successor is unreachable
1295 // and the generator becomes reducible.
1296 std::vector<T> b2(L.bufw, zero);
1297 for (std::size_t b = 2; b <= pos + 1; ++b) b2[b] = buf[b - 2];
1298 for (std::size_t b = pos + 2; b < L.bufw; ++b) b2[b] = buf[b];
1299 if (resume) {
1300 std::vector<T> s3 = srv;
1301 s3[L.Ks[hc - 1] + kst - 1] += one;
1302 std::vector<T> row = b2;
1303 row.insert(row.end(), s3.begin(), s3.end());
1304 row.insert(row.end(), var.begin(), var.end());
1305 out.space.push_back(row);
1306 out.rate.push_back(T(lld * rate));
1307 out.prob.push_back(one);
1308 tag_last(out, hc, 0); // the promoted job resumes on the freed server
1309 } else {
1310 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1311 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1312 std::vector<T> s3 = srv;
1313 s3[L.Ks[hc - 1] + ke] += one;
1314 std::vector<T> row = b2;
1315 row.insert(row.end(), s3.begin(), s3.end());
1316 row.insert(row.end(), var.begin(), var.end());
1317 out.space.push_back(row);
1318 out.rate.push_back(T(lld * rate * pentry[ke]));
1319 out.prob.push_back(one);
1320 tag_last(out, hc, 0);
1321 }
1322 }
1323 } else if (state_detail::buffer_is_per_class_count(sched)) {
1324 // A per-class-count buffer promotes by COUNT, since no order is
1325 // recorded. SIRO picks the next job at random, so every waiting
1326 // class is a distinct successor with its share of the queue.
1327 srv[col] -= one;
1328 const T rate = T(mu[k] * phi[k] * kir);
1329 double waiting = 0;
1330 for (std::size_t r = 0; r < R; ++r) waiting += num_traits<T>::to_double(buf[r]);
1331 if (waiting <= 0 || suppress_promote) {
1332 std::vector<T> row = buf;
1333 row.insert(row.end(), srv.begin(), srv.end());
1334 row.insert(row.end(), var.begin(), var.end());
1335 out.space.push_back(row);
1336 out.rate.push_back(T(lld * rate));
1337 out.prob.push_back(one);
1338 continue;
1339 }
1340 for (std::size_t r = 1; r <= R; ++r) {
1341 const double nb = num_traits<T>::to_double(buf[r - 1]);
1342 if (nb <= 0) continue;
1343 const std::vector<T> pentry = entry_phase_dist(sn, ist, r);
1344 for (std::size_t ke = 0; ke < L.K[r - 1]; ++ke) {
1345 std::vector<T> b2 = buf, s2 = srv;
1346 b2[r - 1] -= one;
1347 s2[L.Ks[r - 1] + ke] += one;
1348 std::vector<T> row = b2;
1349 row.insert(row.end(), s2.begin(), s2.end());
1350 row.insert(row.end(), var.begin(), var.end());
1351 out.space.push_back(row);
1352 out.rate.push_back(T(lld * rate));
1353 out.prob.push_back(T(num_traits<T>::from_double(nb / waiting) * pentry[ke]));
1354 tag_last(out, r, 0); // the drawn waiting class takes the server
1355 }
1356 }
1357 } else {
1358 throw UnsupportedError(
1359 std::string("after_event_station_dep: the ") + lang::sched_to_text(sched) +
1360 " discipline is not ported yet");
1361 }
1362 }
1363 pad_tags(out);
1364 return out;
1365}
1366
1367/**
1368 * The fraction of the station's capacity a class-`cls` job in phase `k`
1369 * receives, which is the only part of the rate the discipline decides.
1370 *
1371 * PHASE and DEP share this factor exactly: an internal phase transition is
1372 * driven by the same server share as a completion, which is why a job under PS
1373 * advances through its phases more slowly when the station is busy. Only the
1374 * MATRIX differs -- D0(k,kdest) for a phase advance, D1 for a completion.
1375 */
1376template <class T>
1377T service_share(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
1378 std::size_t cls, double ni, double S) {
1379 const std::size_t R = sn.nclasses;
1380 const SchedStrategy sched = sn.stations[ist - 1].sched;
1381 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1382
1383 // The *PRIO variants behave as their base discipline while every job fits
1384 // in a server -- with n <= c nobody is waiting, so precedence is moot. Once
1385 // the station saturates, only the MOST URGENT group present is served, and
1386 // the sharing is computed among that group alone. `prio_pop` is the single
1387 // definition of that group, shared with the load-dependent lookup.
1388 const PrioPop<T> p = prio_pop(sn, ist, m, cls, ni, S);
1389 if (!p.served) return zero; // a lower-priority class gets no service at all
1390 const std::vector<T>& nir = p.nir;
1391 const double nieff = p.ni;
1392
1393 if (sched == SchedStrategy::PS || sched == SchedStrategy::LPS ||
1394 sched == SchedStrategy::PSPRIO)
1395 return nieff > 0 ? num_traits<T>::from_double(std::min(nieff, S) / nieff) : zero;
1396 if (sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
1397 sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO) {
1398 if (S > 1)
1399 throw UnsupportedError(
1400 "state_events: multi-server DPS/GPS stations are not supported");
1401 const bool dps = sched == SchedStrategy::DPS || sched == SchedStrategy::DPSPRIO;
1402 const std::vector<T>& w = sn.stations[ist - 1].schedparam;
1403 T wsum = zero;
1404 for (std::size_t r = 0; r < R; ++r) wsum += w[r];
1405 T denom = zero;
1406 for (std::size_t r = 0; r < R; ++r) {
1407 // DPS weights by the job COUNTS; GPS shares between the classes
1408 // PRESENT, so each contributes at most one however many it holds.
1409 const T nr = dps ? nir[r] : (num_traits<T>::to_double(nir[r]) > 0 ? one : zero);
1410 denom += T(w[r] / wsum * nr);
1411 }
1412 const T nc = nir[cls - 1];
1413 if (num_traits<T>::to_double(denom) == 0 || num_traits<T>::to_double(nc) == 0)
1414 return zero;
1415 const T sh = T((w[cls - 1] / wsum) / denom);
1416 return dps ? sh : T(sh / nc);
1417 }
1418 // INF and the queueing disciplines: a job in service holds a whole server.
1419 return one;
1420}
1421
1422/**
1423 * Port of the PHASE branch of `State.afterEventStation`: service advances a
1424 * phase WITHOUT completing.
1425 *
1426 * The rate is D0(k, kdest), the off-diagonal of the hidden generator, times the
1427 * same server share a completion gets. Keeping PHASE and DEP on one share is
1428 * what makes a phase-type service slow down consistently under contention; a
1429 * phase advance at full speed under PS would shorten the effective service.
1430 */
1431template <class T>
1433 const std::vector<T>& inspace, std::size_t cls) {
1434 const std::size_t R = sn.nclasses;
1435 const std::size_t ist = sn.nodes[ind - 1].station;
1436 const T one = num_traits<T>::from_int(1);
1437 EventOutcome<T> out;
1438 if (ist == 0) throw InputError("after_event_station_phase: node is not a station");
1439 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1440 const double S = sn.stations[ist - 1].nservers;
1441
1442 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1443 for (std::size_t r = 0; r < R; ++r) {
1444 ph[r] = L.K[r];
1445 shift[r] = L.Ks[r];
1446 }
1447 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1448 if (num_traits<T>::to_double(m.nir[cls - 1]) <= 0) return out;
1449
1450 double ni = 0;
1451 for (std::size_t r = 0; r < R; ++r) ni += num_traits<T>::to_double(m.nir[r]);
1452 // Unlike DEP, the PHASE branch takes the UNMASKED population for both
1453 // factors even at a saturated *PRIO station (afterEventStation.m:1688): the
1454 // masking there is a property of the completion rate, not of the lookup, and
1455 // `service_share` already zeroes a non-urgent class's advance.
1456 const T lld = T(lld_factor(sn, ist, ni) * cd_factor(sn, ist, m.nir, cls));
1457 const T share = service_share(sn, ist, m, cls, ni, S);
1458 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
1459 if (d.D0.rows() != L.K[cls - 1]) return out;
1460
1461 for (std::size_t k = 0; k < L.K[cls - 1]; ++k) {
1462 if (num_traits<T>::to_double(inspace[L.bufw + L.Ks[cls - 1] + k]) <= 0) continue;
1463 for (std::size_t kd = 0; kd < L.K[cls - 1]; ++kd) {
1464 if (kd == k) continue; // the diagonal is the exit rate, not a move
1465 std::vector<T> row = inspace;
1466 row[L.bufw + L.Ks[cls - 1] + k] -= one;
1467 row[L.bufw + L.Ks[cls - 1] + kd] += one;
1468 out.space.push_back(row);
1469 out.rate.push_back(T(lld * d.D0(k, kd) * m.kir[cls - 1][k] * share));
1470 out.prob.push_back(one);
1471 }
1472 }
1473 return out;
1474}
1475
1476/**
1477 * Port of the RENEGE branch: a WAITING class-`cls` job abandons the queue.
1478 *
1479 * Patience is exponential, so every waiting job abandons at the same rate and
1480 * the aggregate out of this state is (waiting count) * mu. Which job leaves is
1481 * therefore immaterial -- waiting jobs are exchangeable under memoryless
1482 * patience -- so the reference removes the first tagged slot and re-pads a zero
1483 * on the left, keeping the buffer in the right-aligned form the arrival handler
1484 * expects.
1485 */
1486template <class T>
1488 const std::vector<T>& inspace, std::size_t cls,
1489 const T& impatience_mu) {
1490 const std::size_t R = sn.nclasses;
1491 const std::size_t ist = sn.nodes[ind - 1].station;
1492 EventOutcome<T> out;
1493 if (ist == 0) throw InputError("after_event_station_renege: node is not a station");
1494 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1495
1496 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1497 for (std::size_t r = 0; r < R; ++r) {
1498 ph[r] = L.K[r];
1499 shift[r] = L.Ks[r];
1500 }
1501 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1502 const double waiting = num_traits<T>::to_double(m.nir[cls - 1]) -
1503 num_traits<T>::to_double(m.sir[cls - 1]);
1504 if (waiting <= 0) return out;
1505
1506 const T tag = num_traits<T>::from_int(static_cast<long>(cls));
1507 std::size_t slot = L.bufw;
1508 for (std::size_t b = 0; b < L.bufw; ++b)
1509 if (inspace[b] == tag) { slot = b; break; }
1510 if (slot == L.bufw) return out;
1511
1512 std::vector<T> row = inspace;
1513 for (std::size_t b = slot; b > 0; --b) row[b] = row[b - 1];
1514 row[0] = num_traits<T>::from_int(0);
1515 out.space.push_back(row);
1516 out.rate.push_back(T(num_traits<T>::from_double(waiting) * impatience_mu));
1517 out.prob.push_back(num_traits<T>::from_int(1));
1518 return out;
1519}
1520
1521/**
1522 * Port of the RETRY branch: an ORBITING class-`cls` job retries entry.
1523 *
1524 * The retry succeeds only when a server is free; otherwise the job stays in
1525 * orbit and the event is not generated at all, which is exactly what
1526 * distinguishes a retrial queue from a queue whose buffer is called an orbit.
1527 *
1528 * @param constant_policy CONSTANT retrial: one controller retries for the whole
1529 * orbit, so the rate does NOT scale with the orbit size. Under the
1530 * default LINEAR policy every orbiting job carries its own timer and the
1531 * aggregate rate is (orbit size) * mu.
1532 * @param sn the refreshed network struct
1533 * @param ind index of the station the event fires at
1534 * @param inspace the state the event is applied to
1535 * @param cls class of the retrying job
1536 * @param retrial_mu retrial rate of that class
1537 */
1538template <class T>
1540 const std::vector<T>& inspace, std::size_t cls,
1541 const T& retrial_mu, bool constant_policy = false) {
1542 const std::size_t R = sn.nclasses;
1543 const std::size_t ist = sn.nodes[ind - 1].station;
1544 const T one = num_traits<T>::from_int(1);
1545 EventOutcome<T> out;
1546 if (ist == 0) throw InputError("after_event_station_retry: node is not a station");
1547 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1548 const double S = sn.stations[ist - 1].nservers;
1549
1550 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1551 for (std::size_t r = 0; r < R; ++r) {
1552 ph[r] = L.K[r];
1553 shift[r] = L.Ks[r];
1554 }
1555 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1556 const double orbit = num_traits<T>::to_double(m.nir[cls - 1]) -
1557 num_traits<T>::to_double(m.sir[cls - 1]);
1558 double occ = 0;
1559 for (std::size_t j = 0; j < L.srvw; ++j) occ += num_traits<T>::to_double(inspace[L.bufw + j]);
1560 if (orbit <= 0 || occ >= S) return out;
1561
1562 const T tag = num_traits<T>::from_int(static_cast<long>(cls));
1563 std::size_t slot = L.bufw;
1564 for (std::size_t b = 0; b < L.bufw; ++b)
1565 if (inspace[b] == tag) { slot = b; break; }
1566 if (slot == L.bufw) return out;
1567
1568 const std::vector<T> pentry = entry_phase_dist(sn, ist, cls);
1569 const T agg = constant_policy ? retrial_mu
1570 : T(num_traits<T>::from_double(orbit) * retrial_mu);
1571 for (std::size_t ke = 0; ke < L.K[cls - 1]; ++ke) {
1572 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
1573 std::vector<T> row = inspace;
1574 for (std::size_t b = slot; b > 0; --b) row[b] = row[b - 1];
1575 row[0] = num_traits<T>::from_int(0);
1576 row[L.bufw + L.Ks[cls - 1] + ke] += one;
1577 out.space.push_back(row);
1578 out.rate.push_back(T(agg * pentry[ke]));
1579 out.prob.push_back(one);
1580 // A successful retry is the only way into the server at a retrial
1581 // station (its departures never promote from the orbit), so it carries
1582 // the START the startRate == TN + preemptRate identity needs.
1583 tag_last(out, cls, 0);
1584 }
1585 pad_tags(out);
1586 return out;
1587}
1588
1589/**
1590 * Port of the FAILURE and REPAIR branches: the server goes down, or comes back.
1591 *
1592 * Only the STATUS column moves, which is the trailing local variable. Jobs in
1593 * service are NOT lost: service is memoryless here, so an interrupted job
1594 * resumes on repair with no state to remember. The passive half of the
1595 * synchronization is LOCAL, so no job moves anywhere in the network either.
1596 *
1597 * @param up true for REPAIR (0 -> 1), false for FAILURE (1 -> 0)
1598 * @param mu breakdownMu for a failure, repairMu for a repair
1599 * @param sn the refreshed network struct
1600 * @param ind index of the station the event fires at
1601 * @param inspace the state the event is applied to
1602 */
1603template <class T>
1605 const std::vector<T>& inspace, bool up,
1606 const T& mu) {
1607 EventOutcome<T> out;
1608 if (inspace.empty() || !sn.has_breakdown_node(ind)) return out;
1609 const double status = num_traits<T>::to_double(inspace.back());
1610 // A failure needs an UP server and a repair a DOWN one; anything else is
1611 // not an admissible transition and must produce no edge at all.
1612 if ((up && status != 0) || (!up && status != 1)) return out;
1613 std::vector<T> row = inspace;
1614 row.back() = num_traits<T>::from_int(up ? 1 : 0);
1615 // The rate is the station's own breakdownMu / repairMu unless the caller
1616 // supplies one; no caller does today, so the struct is the source of truth.
1617 T rate = mu;
1618 if (!(rate > num_traits<T>::from_int(0))) {
1619 const BreakdownParam<T>& bp = sn.breakdownparam.at(sn.nodes[ind - 1].station);
1620 rate = up ? bp.repair_rate : bp.failure_rate;
1621 }
1622 out.space.push_back(row);
1623 out.rate.push_back(rate);
1624 out.prob.push_back(num_traits<T>::from_int(1));
1625 return out;
1626}
1627
1628/**
1629 * Port of `State.afterEventFork`: an event at a STATEFUL Fork node.
1630 *
1631 * The fork's state is a plain per-class count of PARENT jobs momentarily held
1632 * between their arrival and the firing. An arrival buffers one; a DEPARTURE DOES
1633 * NOT EXIST, because the multi-branch emission is atomic across several nodes and
1634 * cannot be decomposed into a departure here plus an arrival there --
1635 * `refresh_sync` emits no DEP sync for a Fork and `after_fj_event` fires instead.
1636 *
1637 * The arrival rate is left UNSET (the reference writes -1) because an ARV is the
1638 * PASSIVE half of a synchronization: the rate belongs to the active departure
1639 * upstream.
1640 */
1641template <class T>
1643 const std::vector<T>& inspace, EventType event,
1644 std::size_t cls) {
1645 const std::size_t R = sn.nclasses;
1646 EventOutcome<T> out;
1647 if (event != EventType::ARV) return out;
1648 if (inspace.size() < R)
1649 throw InputError("after_event_fork: the Fork state row at node '" + sn.nodes[ind - 1].name +
1650 "' is narrower than the class set");
1651 std::vector<T> row = inspace;
1652 // The counts are the LAST R columns, which is where `from_marginal_node`
1653 // puts them and where `after_fj_event` reads them.
1654 row[row.size() - R + cls - 1] += num_traits<T>::from_int(1);
1655 out.space.push_back(row);
1656 out.rate.push_back(num_traits<T>::from_int(-1));
1657 out.prob.push_back(num_traits<T>::from_int(1));
1658 return out;
1659}
1660
1661/**
1662 * Port of `State.afterEventJoin`: an event at a Join node of an FJ-augmented
1663 * struct.
1664 *
1665 * The join's state is a plain per-class count vector of BUFFERED jobs, so it
1666 * bypasses the buffer/server/local slicing every other station takes -- a join
1667 * performs no service, it performs a rendezvous.
1668 *
1669 * ARV buffers the arriving job or sibling. DEP in an ORIGINAL class r fires when
1670 * either a plain (never-forked) class-r job is buffered, or some tag has its full
1671 * required sibling multiset present; the firing consumes the siblings of the
1672 * LOWEST complete tag, which is the same canonical choice the fork's allocation
1673 * makes and is what keeps the two in step. DEP in an AUXILIARY class is refused
1674 * outright: a sibling never departs on its own, it is consumed by the parent's
1675 * firing, and letting it depart would release a job the fork never emitted.
1676 */
1677template <class T>
1679 const std::vector<T>& inspace, EventType event,
1680 std::size_t cls) {
1681 const std::size_t R = sn.nclasses;
1682 const T one = num_traits<T>::from_int(1);
1683 EventOutcome<T> out;
1684 if (inspace.size() < R)
1685 throw InputError("after_event_join: the Join state row at node '" + sn.nodes[ind - 1].name +
1686 "' is narrower than the class set");
1687 const std::size_t off = inspace.size() - R;
1688
1689 if (event == EventType::ARV) {
1690 std::vector<T> row = inspace;
1691 row[off + cls - 1] += one;
1692 out.space.push_back(row);
1693 out.rate.push_back(num_traits<T>::from_int(-1));
1694 out.prob.push_back(one);
1695 return out;
1696 }
1697 if (event != EventType::DEP) return out;
1698
1699 const typename std::map<std::size_t, FjJoinParam>::const_iterator jit =
1700 sn.fjjoinparam.find(ind);
1701 const FjJoinParam* fjp = jit == sn.fjjoinparam.end() ? 0 : &jit->second;
1702
1703 if (fjp) {
1704 for (std::size_t x = 0; x < fjp->origclasses.size(); ++x) {
1705 const std::map<std::size_t, std::vector<std::vector<std::size_t>>>::const_iterator ait =
1706 fjp->auxmatrix.find(fjp->origclasses[x]);
1707 if (ait == fjp->auxmatrix.end()) continue;
1708 for (std::size_t b = 0; b < ait->second.size(); ++b)
1709 for (std::size_t t = 0; t < ait->second[b].size(); ++t)
1710 if (ait->second[b][t] == cls) return out; // an auxiliary class
1711 }
1712 }
1713
1715 if (num_traits<T>::to_double(inspace[off + cls - 1]) > 0) {
1716 // A plain job that never went through the fork: it passes straight
1717 // through, since a join is a rendezvous only for siblings.
1718 std::vector<T> row = inspace;
1719 row[off + cls - 1] -= one;
1720 out.space.push_back(row);
1721 out.rate.push_back(imm);
1722 out.prob.push_back(one);
1723 return out;
1724 }
1725 if (!fjp) return out;
1726 const std::map<std::size_t, std::vector<std::vector<std::size_t>>>::const_iterator ait =
1727 fjp->auxmatrix.find(cls);
1728 const std::map<std::size_t, std::vector<std::size_t>>::const_iterator rit =
1729 fjp->required.find(cls);
1730 if (ait == fjp->auxmatrix.end() || rit == fjp->required.end()) return out;
1731 const std::vector<std::vector<std::size_t>>& aux = ait->second;
1732 const std::vector<std::size_t>& req = rit->second;
1733 if (aux.empty()) return out;
1734 const std::size_t B = aux.size(), Tt = aux[0].size();
1735 for (std::size_t t = 0; t < Tt; ++t) {
1736 bool complete = true;
1737 for (std::size_t b = 0; b < B && complete; ++b) {
1738 const std::size_t a = aux[b][t];
1739 const double need = b < req.size() ? static_cast<double>(req[b]) : 1.0;
1740 if (num_traits<T>::to_double(inspace[off + a - 1]) < need) complete = false;
1741 }
1742 if (!complete) continue;
1743 std::vector<T> row = inspace;
1744 for (std::size_t b = 0; b < B; ++b) {
1745 const std::size_t a = aux[b][t];
1746 const long need = b < req.size() ? static_cast<long>(req[b]) : 1;
1747 row[off + a - 1] -= num_traits<T>::from_int(need);
1748 }
1749 out.space.push_back(row);
1750 out.rate.push_back(imm);
1751 out.prob.push_back(one);
1752 return out; // the LOWEST complete tag only; the rest are permutations
1753 }
1754 return out;
1755}
1756
1757/**
1758 * Port of `State.afterEventStation`'s dispatch: the successors of one event at
1759 * one station.
1760 *
1761 * The event-specific rates that are not derivable from `sn` alone -- patience,
1762 * retrial and breakdown -- are passed in, because the reference reads them from
1763 * fields (`impatienceMu`, `retrialMu`, `breakdownMu`) that this port has not
1764 * yet grown. Every rate that IS derivable is computed from the struct.
1765 */
1766template <class T>
1767void rr_advance_row(const NetworkStruct<T>& sn, std::size_t ind, std::size_t cls,
1768 std::vector<std::vector<T>>& rows);
1769
1770/**
1771 * Port of `State.afterEventRouter`: a Router holds a job for the instant it
1772 * takes to decide where it goes.
1773 *
1774 * The row is [per-class counts | local vars], with no buffer and no phase: a
1775 * Router serves nothing, so there is no service state to carry. An ARRIVAL adds
1776 * the job at an UNSPECIFIED rate (-1), which is the reference's marker for a
1777 * passive half whose rate the active half sets; a DEPARTURE removes it at the
1778 * Immediate rate and advances the dispatch pointer, so the router never holds a
1779 * job for a positive length of time.
1780 */
1781template <class T>
1783 const std::vector<T>& inspace, EventType event,
1784 std::size_t cls) {
1785 EventOutcome<T> out;
1786 const std::size_t R = sn.nclasses;
1787 if (inspace.size() < R) return out;
1788 const T one = num_traits<T>::from_int(1);
1789 if (event == EventType::ARV) {
1790 std::vector<T> row = inspace;
1791 row[cls - 1] = T(row[cls - 1] + one);
1792 out.space.push_back(row);
1793 // Passive action: the rate is the active half's, not this node's.
1794 out.rate.push_back(num_traits<T>::from_int(-1));
1795 out.prob.push_back(one);
1796 return out;
1797 }
1798 if (event == EventType::DEP) {
1799 if (!(num_traits<T>::to_double(inspace[cls - 1]) > 0)) return out;
1800 std::vector<T> row = inspace;
1801 row[cls - 1] = T(row[cls - 1] - one);
1802 out.space.push_back(row);
1804 out.prob.push_back(one);
1805 rr_advance_row(sn, ind, cls, out.space);
1806 return out;
1807 }
1808 return out;
1809}
1810
1811/**
1812 * Advance the round-robin dispatch pointer of (IND, CLS) in every successor row.
1813 *
1814 * The local-variable block is the TAIL of a state row, so the pointer is located
1815 * from the right; `rr_var_slot` gives its 1-based index inside that block. A
1816 * no-op wherever the pair does not dispatch round-robin.
1817 */
1818template <class T>
1819void rr_advance_row(const NetworkStruct<T>& sn, std::size_t ind, std::size_t cls,
1820 std::vector<std::vector<T>>& rows) {
1821 if (sn.rr_var_slot(ind, cls) == 0) return;
1822 const std::size_t w = sn.nvars_of(ind);
1823 if (w == 0) return;
1824 for (std::size_t i = 0; i < rows.size(); ++i) {
1825 if (rows[i].size() < w) continue;
1826 std::vector<T> var(rows[i].end() - w, rows[i].end());
1827 sn.rr_advance(ind, cls, var);
1828 std::copy(var.begin(), var.end(), rows[i].end() - w);
1829 }
1830}
1831
1832template <class T>
1834 const std::vector<T>& inspace, EventType event,
1835 std::size_t cls, bool no_promote = false,
1836 const T& aux_rate = num_traits<T>::from_int(0)) {
1837 // SERVER BREAKDOWN, `afterEventStation.m:36-65`. A down server (trailing
1838 // status 0) does not serve, so DEP and PHASE produce nothing -- unless a
1839 // degraded down-server rate is configured for the class, in which case the
1840 // up-server outcome is computed below and its rates rescaled by
1841 // downRate/upRate. Gating here keeps every scheduling branch unaware of it.
1842 if ((event == EventType::DEP || event == EventType::PHASE) && !inspace.empty() &&
1843 sn.has_breakdown_node(ind) && num_traits<T>::to_double(inspace.back()) == 0) {
1844 const std::size_t ist = sn.nodes[ind - 1].station;
1845 const T down = sn.breakdownparam.at(ist).down_rate_of(cls);
1846 if (!(down > num_traits<T>::from_int(0))) return EventOutcome<T>();
1847 const T upr = sn.rates(ist - 1, cls - 1);
1848 const double upd = num_traits<T>::to_double(upr);
1849 if (!std::isfinite(upd) || !(upd > 0.0))
1850 throw InputError("Station '" + sn.nodes[ind - 1].name +
1851 "' declares a down-server service rate for class '" +
1852 sn.classes[cls - 1].name +
1853 "' but has no finite up-server service rate to rescale.");
1854 // Evaluate the UP handler on the same row with the status set to up, then
1855 // put the status back to down in every successor: the outage does not end
1856 // because a job completed.
1857 std::vector<T> upspace = inspace;
1858 upspace.back() = num_traits<T>::from_int(1);
1859 EventOutcome<T> o =
1860 after_event_station(sn, ind, upspace, event, cls, no_promote, aux_rate);
1861 const T scale = T(down / upr);
1862 for (std::size_t i = 0; i < o.space.size(); ++i) {
1863 if (!o.space[i].empty()) o.space[i].back() = num_traits<T>::from_int(0);
1864 o.rate[i] = T(o.rate[i] * scale);
1865 }
1866 return o;
1867 }
1868 switch (event) {
1869 case EventType::ARV:
1870 // A signal class arriving is not an arrival at all: it removes
1871 // jobs and is annihilated, so it never reaches the scheduling
1872 // branches. A REPLY signal is the exception -- it completes a
1873 // synchronous call and then joins as an ordinary job.
1874 if (sn.issignal.size() >= cls && sn.issignal[cls - 1]) {
1875 if (!(sn.signaltype.size() >= cls &&
1876 sn.signaltype[cls - 1] == lang::SignalType::REPLY))
1877 return after_event_station_signal(sn, ind, inspace, cls);
1878 // A REPLY takes the reply path only at a station that actually
1879 // holds a block for it; elsewhere it is a plain job class and
1880 // falls through to ordinary arrival handling.
1881 if (reply_block_info(sn, ind).width > 0)
1882 return after_event_station_reply(sn, ind, inspace, cls);
1883 }
1884 return after_event_station_arv(sn, ind, inspace, cls);
1885 case EventType::DEP: {
1886 EventOutcome<T> out = after_event_station_dep(sn, ind, inspace, cls, no_promote);
1887 // ROUND-ROBIN DISPATCH advances on every completion, which is what
1888 // makes the next destination deterministic; the generator then reads
1889 // the pointer OUT OF THIS SUCCESSOR to pick the link. Without the
1890 // advance the pointer is a frozen coordinate and every job takes the
1891 // same link, which is random routing with the wrong support rather
1892 // than round robin. Reference: `afterEventStation.m:632-658`.
1893 rr_advance_row(sn, ind, cls, out.space);
1894 // TRUE BAS, the departure half. When the marker is already set the
1895 // front job has COMPLETED and is being held, so this DEP is not a
1896 // service completion at all -- it is the instant transfer of that
1897 // held job downstream, which the generator only offers when the
1898 // destination has room. It therefore fires at the Immediate rate and
1899 // CLEARS the marker; the complementary become-blocked edge (0 -> 1) is
1900 // added by the generator, the only place that can see the
1901 // destination's occupancy.
1902 //
1903 // Gate on `isbasblocking`, not on this station's own drop rule: under
1904 // the destination declaration form the rule is not here.
1905 if (ind <= sn.isbasblocking.size() && sn.isbasblocking[ind - 1] &&
1906 !inspace.empty() && num_traits<T>::to_double(inspace.back()) == 1 &&
1907 !out.space.empty()) {
1908 // 1e7 VERBATIM, not GlobalConstants::Immediate (1e8). The rate is
1909 // large but FINITE, so the blocked states keep a proportional
1910 // share of the stationary mass and the analyzer's queue-length
1911 // shift reads it; using 1e8 would divide that share by ten and
1912 // move every reported queue length. The reference hardcodes 1e7.
1913 const T imm = num_traits<T>::from_double(1e7);
1914 for (std::size_t i = 0; i < out.space.size(); ++i) {
1915 out.space[i].back() = num_traits<T>::from_int(0);
1916 out.rate[i] = imm;
1917 }
1918 }
1919 return out;
1920 }
1921 case EventType::PHASE:
1922 return after_event_station_phase(sn, ind, inspace, cls);
1923 case EventType::RENEGE:
1924 return after_event_station_renege(sn, ind, inspace, cls, aux_rate);
1925 case EventType::RETRY:
1926 return after_event_station_retry(sn, ind, inspace, cls, aux_rate);
1927 case EventType::SWITCH:
1928 return after_event_station_switch(sn, ind, inspace, cls);
1929 case EventType::FAILURE:
1930 return after_event_station_breakdown(sn, ind, inspace, false, aux_rate);
1931 case EventType::REPAIR:
1932 return after_event_station_breakdown(sn, ind, inspace, true, aux_rate);
1933 case EventType::LOCAL:
1934 return EventOutcome<T>(); // a dummy event moves nothing
1935 default:
1936 throw UnsupportedError(std::string("after_event_station: the ") +
1937 lang::event_to_text(event) +
1938 " event is not ported yet");
1939 }
1940}
1941
1942/**
1943 * Port of `State.afterEventTransition`, the PHASE arm: one running server of
1944 * the given mode advances its firing phase (`cls` is interpreted as the MODE,
1945 * as in the reference). ENABLE and FIRE are global events handled by
1946 * `after_global_event`, so they return an empty outcome here, matching the
1947 * reference's no-op arms.
1948 *
1949 * RATE. The MATLAB body multiplies the phase-k move rate by BOTH
1950 * kir(:,mode,k) and nir(mode) (afterEventTransition.m:38-40), but nir is the
1951 * sum of kir over the phases, so the extra factor counts the running servers
1952 * twice; the JAR and the native python carry D0(k,kdest) * kir alone, and
1953 * this port follows them.
1954 *
1955 * The row layout is the one `after_global_event` slices:
1956 * [buf(nmodes) | srv(sum fK) | fired(nmodes) | var].
1957 */
1958template <class T>
1960 const std::vector<T>& inspace, EventType event,
1961 std::size_t mode) {
1962 EventOutcome<T> out;
1963 if (event != EventType::PHASE) return out; // ENABLE / FIRE are global
1964 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
1965 sn.transparam.find(ind);
1966 if (it == sn.transparam.end())
1967 throw InputError("after_event_transition: node has no TransitionParam");
1968 const TransitionParam<T>& tp = it->second;
1969 if (mode == 0 || mode > tp.nmodes)
1970 throw InputError("after_event_transition: mode index is out of range");
1971 const T one = num_traits<T>::from_int(1);
1972
1973 std::vector<std::size_t> fK(tp.nmodes, 1), fKs(tp.nmodes, 0);
1974 std::size_t tot = 0;
1975 for (std::size_t m = 0; m < tp.nmodes; ++m) {
1976 fK[m] = m < tp.firingphases.size() && tp.firingphases[m] > 0 ? tp.firingphases[m] : 1;
1977 fKs[m] = tot;
1978 tot += fK[m];
1979 }
1980 if (fK[mode - 1] <= 1) return out; // a single phase has no internal move
1981 if (mode - 1 >= tp.firingproc.size() ||
1982 tp.firingproc[mode - 1].D0.rows() != fK[mode - 1])
1983 return out;
1984 const Matrix<T>& D0 = tp.firingproc[mode - 1].D0;
1985
1986 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
1987 const std::size_t idx = tp.nmodes + fKs[mode - 1] + k;
1988 const T cnt = inspace[idx];
1989 if (!(num_traits<T>::to_double(cnt) > 0)) continue;
1990 for (std::size_t kd = 0; kd < fK[mode - 1]; ++kd) {
1991 if (kd == k) continue; // the diagonal is the exit rate, not a move
1992 if (!(num_traits<T>::to_double(D0(k, kd)) > 0)) continue;
1993 std::vector<T> row = inspace;
1994 row[idx] -= one;
1995 row[tp.nmodes + fKs[mode - 1] + kd] += one;
1996 out.space.push_back(row);
1997 out.rate.push_back(T(D0(k, kd) * cnt));
1998 out.prob.push_back(one);
1999 }
2000 }
2001 return out;
2002}
2003
2004/**
2005 * Port of `State.afterEvent`: the successors of one event at one NODE.
2006 *
2007 * The reference's body is mostly slicing -- it cuts `inspace` into buffer,
2008 * server and local-variable blocks and hands the pieces to the per-node-type
2009 * handler. This port slices inside each handler instead (`row_layout`), so what
2010 * remains here is the dispatch itself and the guards that precede it.
2011 *
2012 * A class the station does not accept short-circuits: `phases_of` is zero
2013 * there, and every downstream index into the server block would be out of
2014 * range. That guard is the reference's `K(class) == 0` test.
2015 *
2016 * `cls` IS A MODE, NOT A CLASS, on a Transition's PHASE action; see the guard.
2017 */
2018template <class T>
2020 const std::vector<T>& inspace, EventType event, std::size_t cls,
2021 bool no_promote = false,
2022 const T& aux_rate = num_traits<T>::from_int(0)) {
2023 if (ind == 0 || ind > sn.nodes.size())
2024 throw InputError("after_event: node index is out of range");
2025 const NodeDef& nd = sn.nodes[ind - 1];
2026 // THE `cls` SLOT IS NOT ALWAYS A CLASS. On a Transition's PHASE action it
2027 // carries the MODE, which `refresh_sync` puts there deliberately ("one
2028 // server phase-change action per MODE, not per class") and
2029 // `after_event_transition` reads back as such. Bounding it by `nclasses`
2030 // refused every mode past the class count: spn_basic_closed is one class
2031 // and three modes, so modes 2 and 3 raised "class index is out of range"
2032 // and no closed SPN with more modes than classes could be walked at all.
2033 // The mode bound belongs to the handler, which already applies it against
2034 // `tp.nmodes`, so only the non-Transition case is checked here.
2035 const bool cls_is_mode = (nd.nodetype == NodeType::Transition && event == EventType::PHASE);
2036 if (!cls_is_mode && (cls == 0 || cls > sn.nclasses))
2037 throw InputError("after_event: class index is out of range");
2038
2039 // A Join of an FJ-augmented struct IS a station, but its state is a bare
2040 // per-class count vector: it performs a rendezvous, not a service, so it must
2041 // bypass the buffer/server slicing before `after_event_station` sees it.
2042 if (sn.isfjaugmented && nd.nodetype == NodeType::Join)
2043 return after_event_join(sn, ind, inspace, event, cls);
2044
2045 if (nd.station != 0) {
2046 // A class with no service process at this station cannot be involved
2047 // in any event here.
2048 if (sn.phases_of(nd.station, cls) == 0) return EventOutcome<T>();
2049 return after_event_station(sn, ind, inspace, event, cls, no_promote, aux_rate);
2050 }
2051 if (!nd.stateful) return EventOutcome<T>(); // a stateless node holds nothing
2052
2053 if (nd.nodetype == NodeType::Cache) return after_event_cache(sn, ind, inspace, event, cls);
2054 if (nd.nodetype == NodeType::Fork) return after_event_fork(sn, ind, inspace, event, cls);
2055 if (nd.nodetype == NodeType::Transition)
2056 return after_event_transition(sn, ind, inspace, event, cls);
2057 if (nd.nodetype == NodeType::Router) return after_event_router(sn, ind, inspace, event, cls);
2058
2059 throw UnsupportedError("after_event: events at stateful non-station node '" + nd.name +
2060 "' are not ported yet");
2061}
2062
2063namespace signal_detail {
2064
2065/** Merge duplicate destinations so the generator sees one entry per state. */
2066template <class T>
2067void merge_states(std::vector<std::vector<T>>& sp, std::vector<T>& pr) {
2068 std::vector<std::vector<T>> us;
2069 std::vector<T> up;
2070 for (std::size_t i = 0; i < sp.size(); ++i) {
2071 std::size_t at = us.size();
2072 for (std::size_t j = 0; j < us.size(); ++j)
2073 if (us[j] == sp[i]) { at = j; break; }
2074 if (at == us.size()) {
2075 us.push_back(sp[i]);
2076 up.push_back(pr[i]);
2077 } else {
2078 up[at] += pr[i];
2079 }
2080 }
2081 sp.swap(us);
2082 pr.swap(up);
2083}
2084
2085} // namespace signal_detail
2086
2087/**
2088 * Port of `State.signalBatchPMF`: the batch size a negative signal removes.
2089 *
2090 * The pmf is CLIPPED at the eligible population: an oversized batch empties
2091 * the station rather than driving the queue negative, so the whole tail
2092 * P(B >= n) lumps onto "remove all n". That is the same clipping LDES applies
2093 * with min(B,n) and the tail term MAM uses.
2094 */
2095template <class T>
2096std::pair<std::vector<std::size_t>, std::vector<T>> signal_batch_pmf(
2097 const NetworkStruct<T>& sn, std::size_t cls, std::size_t ntot) {
2098 std::vector<std::size_t> kv;
2099 std::vector<T> kp;
2100 if (sn.signalremdist.size() < cls || sn.signalremdist[cls - 1].empty()) {
2101 kv.push_back(1);
2102 kp.push_back(num_traits<T>::from_int(1));
2103 return std::make_pair(kv, kp);
2104 }
2105 const std::vector<T>& d = sn.signalremdist[cls - 1];
2106 T head_sum = num_traits<T>::from_int(0);
2107 for (std::size_t b = 0; b < ntot; ++b) {
2108 const T p = b < d.size() ? d[b] : num_traits<T>::from_int(0);
2109 if (num_traits<T>::to_double(p) > 0) {
2110 kv.push_back(b);
2111 kp.push_back(p);
2112 }
2113 head_sum += p;
2114 }
2115 const double tail = 1.0 - num_traits<T>::to_double(head_sum);
2116 if (tail > 0) {
2117 kv.push_back(ntot);
2118 kp.push_back(num_traits<T>::from_double(tail));
2119 }
2120 if (kv.empty()) {
2121 kv.push_back(1);
2122 kp.push_back(num_traits<T>::from_int(1));
2123 return std::make_pair(kv, kp);
2124 }
2125 T tot = num_traits<T>::from_int(0);
2126 for (std::size_t i = 0; i < kp.size(); ++i) tot += kp[i];
2127 if (num_traits<T>::to_double(tot) > 0)
2128 for (std::size_t i = 0; i < kp.size(); ++i) kp[i] = T(kp[i] / tot);
2129 return std::make_pair(kv, kp);
2130}
2131
2132/**
2133 * Port of `State.afterEventStationSignal`: a G-network signal arrives.
2134 *
2135 * A signal NEVER joins the station. It removes jobs already there and is
2136 * annihilated, so the event is passive throughout and the successors differ
2137 * only in which victims were taken.
2138 *
2139 * Victim selection has two tiers, and conflating them is the trap: FCFS and
2140 * LCFS rank by AGE, which only an ordered buffer records, so at a per-class
2141 * count buffer an age policy degenerates to a uniform draw. They also drain
2142 * the waiting line completely before touching a server, whereas RANDOM draws
2143 * uniformly across waiting and in-service jobs alike.
2144 */
2145template <class T>
2147 const std::vector<T>& inspace, std::size_t cls) {
2148 const std::size_t R = sn.nclasses;
2149 const std::size_t ist = sn.nodes[ind - 1].station;
2150 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
2151 const T minus_one = num_traits<T>::from_int(-1);
2152 EventOutcome<T> out;
2153 if (ist == 0) throw InputError("after_event_station_signal: node is not a station");
2154 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2155 const SchedStrategy sched = sn.stations[ist - 1].sched;
2156 const double S = sn.stations[ist - 1].nservers;
2157
2158 std::vector<std::size_t> ph(R, 1), shift(R, 0);
2159 for (std::size_t r = 0; r < R; ++r) {
2160 ph[r] = L.K[r];
2161 shift[r] = L.Ks[r];
2162 }
2163 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
2164
2165 // A CATASTROPHE empties the station outright, ignoring the batch pmf: a
2166 // catastrophe removes every job by definition.
2167 if (sn.signaltype.size() >= cls && sn.signaltype[cls - 1] == lang::SignalType::CATASTROPHE) {
2168 std::vector<T> row(L.bufw + L.srvw, zero);
2169 row.insert(row.end(), inspace.end() - L.nvar, inspace.end());
2170 out.space.push_back(row);
2171 out.rate.push_back(minus_one);
2172 out.prob.push_back(one);
2173 return out;
2174 }
2175
2176 // Eligible victim classes: the declared target, or every non-signal class
2177 // for the classic untargeted negative customer.
2178 std::vector<std::size_t> tgt;
2179 const std::size_t declared = sn.signaltarget.size() >= cls ? sn.signaltarget[cls - 1] : 0;
2180 if (declared >= 1) {
2181 tgt.push_back(declared);
2182 } else {
2183 for (std::size_t r = 1; r <= R; ++r)
2184 if (sn.issignal.size() < r || !sn.issignal[r - 1]) tgt.push_back(r);
2185 }
2186 std::vector<std::size_t> elig;
2187 std::size_t ntot = 0;
2188 for (std::size_t i = 0; i < tgt.size(); ++i) {
2189 const double nr = num_traits<T>::to_double(m.nir[tgt[i] - 1]);
2190 if (nr > 0) {
2191 elig.push_back(tgt[i]);
2192 ntot += static_cast<std::size_t>(nr);
2193 }
2194 }
2195 if (elig.empty()) {
2196 // No victim: the signal simply vanishes, leaving the state unchanged.
2197 out.space.push_back(inspace);
2198 out.rate.push_back(minus_one);
2199 out.prob.push_back(one);
2200 return out;
2201 }
2202
2203 const lang::RemovalPolicy policy = sn.signalrempolicy.size() >= cls
2204 ? sn.signalrempolicy[cls - 1]
2206 const bool ordered = state_detail::buffer_is_class_tag(sched) ||
2207 state_detail::buffer_is_tag_phase_pairs(sched);
2208 const bool paired = state_detail::buffer_is_tag_phase_pairs(sched);
2209 const bool counted = state_detail::buffer_is_per_class_count(sched);
2210 const std::pair<std::vector<std::size_t>, std::vector<T>> pmf =
2211 signal_batch_pmf(sn, cls, ntot);
2212
2213 std::vector<std::vector<T>> acc;
2214 std::vector<T> accp;
2215 for (std::size_t ik = 0; ik < pmf.first.size(); ++ik) {
2216 if (num_traits<T>::to_double(pmf.second[ik]) <= 0) continue;
2217 std::vector<std::vector<T>> cur(1, inspace);
2218 std::vector<T> curp(1, one);
2219 // Remove one job at a time: sequential draws without replacement give
2220 // a uniform choice of the removed SUBSET.
2221 for (std::size_t step = 0; step < pmf.first[ik]; ++step) {
2222 std::vector<std::vector<T>> nxt;
2223 std::vector<T> nxtp;
2224 for (std::size_t row = 0; row < cur.size(); ++row) {
2225 std::vector<T> buf(cur[row].begin(), cur[row].begin() + L.bufw);
2226 std::vector<T> srv(cur[row].begin() + L.bufw,
2227 cur[row].begin() + L.bufw + L.srvw);
2228 const std::vector<T> var(cur[row].begin() + L.bufw + L.srvw, cur[row].end());
2229
2230 // Waiting victims, as (position, class, multiplicity).
2231 std::vector<std::size_t> wpos, wcls, wwt;
2232 if (ordered) {
2233 for (std::size_t b = 0; b < L.bufw; b += paired ? 2 : 1) {
2234 const double v = num_traits<T>::to_double(buf[b]);
2235 if (v <= 0) continue;
2236 bool ok = false;
2237 for (std::size_t e = 0; e < elig.size(); ++e)
2238 if (elig[e] == static_cast<std::size_t>(v)) ok = true;
2239 if (!ok) continue;
2240 wpos.push_back(b);
2241 wcls.push_back(static_cast<std::size_t>(v));
2242 wwt.push_back(1);
2243 }
2244 } else if (counted) {
2245 for (std::size_t e = 0; e < elig.size(); ++e) {
2246 const std::size_t r = elig[e];
2247 if (r > L.bufw) continue;
2248 const double v = num_traits<T>::to_double(buf[r - 1]);
2249 if (v <= 0) continue;
2250 wpos.push_back(r - 1);
2251 wcls.push_back(r);
2252 wwt.push_back(static_cast<std::size_t>(v));
2253 }
2254 }
2255 // In-service victims, per (class, phase).
2256 std::vector<std::size_t> scls, sph, scnt;
2257 for (std::size_t e = 0; e < elig.size(); ++e) {
2258 const std::size_t r = elig[e];
2259 for (std::size_t p = 0; p < L.K[r - 1]; ++p) {
2260 const double v = num_traits<T>::to_double(srv[L.Ks[r - 1] + p]);
2261 if (v <= 0) continue;
2262 scls.push_back(r);
2263 sph.push_back(p);
2264 scnt.push_back(static_cast<std::size_t>(v));
2265 }
2266 }
2267 std::size_t nwait = 0, nsrv = 0;
2268 for (std::size_t i = 0; i < wwt.size(); ++i) nwait += wwt[i];
2269 for (std::size_t i = 0; i < scnt.size(); ++i) nsrv += scnt[i];
2270 if (nwait == 0 && nsrv == 0) {
2271 // Already drained: nothing left for this step to remove.
2272 nxt.push_back(cur[row]);
2273 nxtp.push_back(curp[row]);
2274 continue;
2275 }
2276
2277 std::vector<std::vector<T>> sp;
2278 std::vector<T> pr;
2279 const bool age_ordered =
2280 ordered && (policy == lang::RemovalPolicy::FCFS ||
2281 policy == lang::RemovalPolicy::LCFS);
2282 if (age_ordered && nwait > 0) {
2283 // The head of line is the LAST occupied slot, the newest
2284 // arrival the first: the buffer is right-aligned.
2285 std::size_t pick = 0;
2286 for (std::size_t i = 0; i < wpos.size(); ++i)
2287 if (policy == lang::RemovalPolicy::FCFS ? wpos[i] > wpos[pick]
2288 : wpos[i] < wpos[pick])
2289 pick = i;
2290 std::vector<T> b2 = buf;
2291 b2.erase(b2.begin() + wpos[pick], b2.begin() + wpos[pick] + (paired ? 2 : 1));
2292 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2293 std::vector<T> nr = b2;
2294 nr.insert(nr.end(), srv.begin(), srv.end());
2295 nr.insert(nr.end(), var.begin(), var.end());
2296 sp.push_back(nr);
2297 pr.push_back(one);
2298 } else {
2299 // RANDOM draws over everything present; FCFS/LCFS at a
2300 // count buffer drain the waiting line first, and only reach
2301 // the servers once it is empty.
2302 const std::size_t total = policy == lang::RemovalPolicy::RANDOM
2303 ? nwait + nsrv
2304 : (nwait > 0 ? nwait : nsrv);
2305 for (std::size_t i = 0; i < wpos.size(); ++i) {
2306 std::vector<T> b2 = buf;
2307 if (counted) {
2308 b2[wcls[i] - 1] -= one;
2309 } else {
2310 b2.erase(b2.begin() + wpos[i],
2311 b2.begin() + wpos[i] + (paired ? 2 : 1));
2312 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2313 }
2314 std::vector<T> nr = b2;
2315 nr.insert(nr.end(), srv.begin(), srv.end());
2316 nr.insert(nr.end(), var.begin(), var.end());
2317 sp.push_back(nr);
2318 pr.push_back(num_traits<T>::from_double(static_cast<double>(wwt[i]) /
2319 static_cast<double>(total)));
2320 }
2321 if (policy == lang::RemovalPolicy::RANDOM || nwait == 0) {
2322 for (std::size_t i = 0; i < scls.size(); ++i) {
2323 std::vector<T> b2 = buf, s2 = srv;
2324 s2[L.Ks[scls[i] - 1] + sph[i]] -= one;
2325 // The freed server pulls in the head of line, where
2326 // the station keeps one at all.
2327 double occ = 0;
2328 for (std::size_t j = 0; j < s2.size(); ++j)
2329 occ += num_traits<T>::to_double(s2[j]);
2330 if (L.bufw > 0 && occ < S) {
2331 if (ordered) {
2332 std::size_t hp = L.bufw;
2333 for (std::size_t b = 0; b < L.bufw; b += paired ? 2 : 1)
2334 if (num_traits<T>::to_double(b2[b]) > 0) hp = b;
2335 if (hp != L.bufw) {
2336 const std::size_t pc = static_cast<std::size_t>(
2337 num_traits<T>::to_double(b2[hp]));
2338 std::size_t pp = 0;
2339 if (paired) {
2340 const double v =
2341 num_traits<T>::to_double(b2[hp + 1]);
2342 pp = v >= 1 ? static_cast<std::size_t>(v) - 1 : 0;
2343 }
2344 b2.erase(b2.begin() + hp,
2345 b2.begin() + hp + (paired ? 2 : 1));
2346 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2347 s2[L.Ks[pc - 1] + pp] += one;
2348 }
2349 } else if (counted) {
2350 // A count buffer carries no order, so the
2351 // lowest-indexed waiting class is promoted
2352 // to keep the map single-valued; the actual
2353 // service order is resolved by the rates.
2354 for (std::size_t r = 1; r <= R && r <= L.bufw; ++r)
2355 if (num_traits<T>::to_double(b2[r - 1]) > 0) {
2356 b2[r - 1] -= one;
2357 s2[L.Ks[r - 1]] += one;
2358 break;
2359 }
2360 }
2361 }
2362 std::vector<T> nr = b2;
2363 nr.insert(nr.end(), s2.begin(), s2.end());
2364 nr.insert(nr.end(), var.begin(), var.end());
2365 sp.push_back(nr);
2366 pr.push_back(num_traits<T>::from_double(
2367 static_cast<double>(scnt[i]) / static_cast<double>(total)));
2368 }
2369 }
2370 }
2371 signal_detail::merge_states(sp, pr);
2372 for (std::size_t i = 0; i < sp.size(); ++i) {
2373 nxt.push_back(sp[i]);
2374 nxtp.push_back(T(curp[row] * pr[i]));
2375 }
2376 }
2377 signal_detail::merge_states(nxt, nxtp);
2378 cur.swap(nxt);
2379 curp.swap(nxtp);
2380 }
2381 for (std::size_t i = 0; i < cur.size(); ++i) {
2382 acc.push_back(cur[i]);
2383 accp.push_back(T(pmf.second[ik] * curp[i]));
2384 }
2385 }
2386 signal_detail::merge_states(acc, accp);
2387 out.space.swap(acc);
2388 out.prob.swap(accp);
2389 out.rate.assign(out.space.size(), minus_one);
2390 return out;
2391}
2392
2393/**
2394 * Port of `State.passAndSwap`: the transition a service completion triggers at
2395 * a pass-and-swap station (Dorsman and Gardner 2024, Sect. 2.3).
2396 *
2397 * The completing job scans FORWARD from its own position for the first job it
2398 * may swap with per the graph G, takes that job's place and ejects it; the
2399 * ejected job repeats the scan. The chain ends at a job with no swappable
2400 * successor, and THAT job departs -- which is why the departing class is in
2401 * general not the class whose service completed.
2402 *
2403 * @param c 0-based list of 1-based class indices, oldest first
2404 * @param p 0-based position whose service token completed
2405 * @param G class-compatibility graph, G[a][b] true when a may swap with b
2406 * @return (the list after the transition, the 1-based departing class)
2407 */
2408template <class T>
2409std::pair<std::vector<std::size_t>, std::size_t> pass_and_swap(
2410 const std::vector<std::size_t>& c, std::size_t p,
2411 const std::vector<std::vector<bool>>& G) {
2412 const std::size_t n = c.size();
2413 if (p >= n) throw InputError("pass_and_swap: position is out of range for the state");
2414 std::vector<std::size_t> chain(1, p);
2415 std::size_t moving = c[p], cur = p;
2416 for (;;) {
2417 std::size_t q = n;
2418 for (std::size_t j = cur + 1; j < n; ++j)
2419 if (moving - 1 < G.size() && c[j] - 1 < G[moving - 1].size() &&
2420 G[moving - 1][c[j] - 1]) {
2421 q = j;
2422 break;
2423 }
2424 if (q == n) break; // no swappable successor: this job departs
2425 chain.push_back(q);
2426 moving = c[q];
2427 cur = q;
2428 }
2429 const std::size_t dep = c[chain.back()];
2430 // Shift classes one step along the chain; the last is overwritten because
2431 // it departed, and the head-of-chain slot is then removed.
2432 std::vector<std::size_t> cnew = c;
2433 for (std::size_t i = 0; i + 1 < chain.size(); ++i) cnew[chain[i + 1]] = c[chain[i]];
2434 cnew.erase(cnew.begin() + chain[0]);
2435 return std::make_pair(cnew, dep);
2436}
2437
2438/**
2439 * Port of `State.afterEventStationPAS`: events at a pass-and-swap station.
2440 *
2441 * The state here is NOT the [buffer | server] split every other discipline
2442 * uses: it is the full ordered list of class indices, left-aligned and zero
2443 * padded, with no server block at all. Service is governed by the rate
2444 * function mu(c) rather than by a per-class rate, so the whole notion of "in
2445 * service" is replaced by a token at each position.
2446 */
2447/**
2448 * Per-position service rate increments Delta_mu(c1..cp) = mu(c1..cp) - mu(c1..c_{p-1}).
2449 */
2450template <class T, class F>
2451inline std::vector<double> pas_increments(const F& mu_fun, const std::vector<std::size_t>& c) {
2452 std::vector<double> inc(c.size(), 0.0);
2453 double mu_prev = 0.0;
2454 for (std::size_t p = 0; p < c.size(); ++p) {
2455 const std::vector<std::size_t> prefix(c.begin(), c.begin() + p + 1);
2456 const double mu_cur = num_traits<T>::to_double(mu_fun(prefix));
2457 inc[p] = mu_cur - mu_prev;
2458 mu_prev = mu_cur;
2459 }
2460 return inc;
2461}
2462
2463/**
2464 * Tag the successor just appended to OUT with the PAS positions that started
2465 * service on it: those of CNEW that are served (Delta_mu > 0) and were not
2466 * served in COLD. Under a swap the tag follows the POSITION rather than the job
2467 * identity, since pass-and-swap redefines which job holds a position.
2468 */
2469template <class T, class F>
2470inline void pas_tag_started(EventOutcome<T>& out, const F& mu_fun,
2471 const std::vector<std::size_t>& cold,
2472 const std::vector<std::size_t>& cnew) {
2473 if (!mu_fun || out.space.empty()) return;
2474 const std::vector<double> inc_new = pas_increments<T>(mu_fun, cnew);
2475 const std::vector<double> inc_old = pas_increments<T>(mu_fun, cold);
2476 for (std::size_t p = 0; p < inc_new.size(); ++p) {
2477 if (inc_new[p] <= 0) continue;
2478 if (p < inc_old.size() && inc_old[p] > 0) continue; // already served
2479 tag_last(out, cnew[p], 0);
2480 }
2481}
2482
2483template <class T>
2485 const std::vector<T>& inspace, EventType event,
2486 std::size_t cls) {
2487 const std::size_t ist = sn.nodes[ind - 1].station;
2488 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2489 EventOutcome<T> out;
2490 if (ist == 0) throw InputError("after_event_station_pas: node is not a station");
2491 const typename std::map<std::size_t, typename NetworkStruct<T>::PasParam>::const_iterator it =
2492 sn.pasparam.find(ist);
2493 if (it == sn.pasparam.end() || !it->second.svc_rate_fun)
2494 throw InputError(
2495 "after_event_station_pas: the station has no service rate function mu(c); set one "
2496 "with set_pas");
2497
2498 const std::size_t V = sn.nvars_of(ind);
2499 const std::size_t W = inspace.size() - V;
2500 std::vector<std::size_t> c;
2501 for (std::size_t i = 0; i < W; ++i) {
2502 const double v = num_traits<T>::to_double(inspace[i]);
2503 if (v > 0) c.push_back(static_cast<std::size_t>(v));
2504 }
2505 const std::vector<T> var(inspace.begin() + W, inspace.end());
2506 const double cap = sn.cap[ist - 1];
2507
2508 if (event == EventType::ARV) {
2509 // The arrival joins at the BACK of the list, which is what records the
2510 // order the rate function is a function of.
2511 if (static_cast<double>(c.size()) >= cap) return out; // full: lost
2512 std::vector<std::size_t> nc = c;
2513 nc.push_back(cls);
2514 // NO SLOT IN THE ENCODING IS A BLOCK, NOT A LOSS. The list occupies one
2515 // row position per job, so a row of width W holds W jobs; emitting a
2516 // successor here and letting `row.resize(W, zero)` cut the list back to
2517 // W would DESTROY the job that did not fit while reporting a state that
2518 // looks exactly like the pre-arrival one -- a customer silently gone
2519 // from a closed network, with no error anywhere. Returning no successor
2520 // disables the upstream departure instead, which is what every other
2521 // discipline's capacity filter does and what the enumerated CTMC space
2522 // does at its own width boundary.
2523 if (nc.size() > W) return out;
2524 std::vector<T> row;
2525 for (std::size_t i = 0; i < nc.size(); ++i)
2526 row.push_back(num_traits<T>::from_int(static_cast<long>(nc[i])));
2527 row.resize(W, zero);
2528 row.insert(row.end(), var.begin(), var.end());
2529 out.space.push_back(row);
2530 out.rate.push_back(num_traits<T>::from_int(-1));
2531 out.prob.push_back(one);
2532 // A PAS station has one clock for the whole station and no server to
2533 // hold, so "in service" means "at a position whose rate increment
2534 // Delta_mu is positive": a job starts exactly when a position goes from
2535 // a zero increment to a positive one. With mu(c) = 1 only the head is
2536 // served (M/M/1), with mu(c) = |c| every position is (M/M/inf), and the
2537 // rule reproduces both.
2538 pas_tag_started<T>(out, it->second.svc_rate_fun, c, nc);
2539 pad_tags(out);
2540 return out;
2541 }
2542 if (event != EventType::DEP) return out; // PAS service is exponential: no PHASE
2543
2544 // Each position holds a service token firing at the INCREMENT of mu over
2545 // the prefix ending there. Summing the increments telescopes to mu(c), so
2546 // the station's total service rate is exactly the rate function.
2547 T mu_prev = zero;
2548 for (std::size_t p = 0; p < c.size(); ++p) {
2549 const std::vector<std::size_t> prefix(c.begin(), c.begin() + p + 1);
2550 const T mu_cur = it->second.svc_rate_fun(prefix);
2551 const T ratep = T(mu_cur - mu_prev);
2552 mu_prev = mu_cur;
2553 if (num_traits<T>::to_double(ratep) <= 0) continue; // position unserved
2554 const std::pair<std::vector<std::size_t>, std::size_t> ps =
2555 pass_and_swap<T>(c, p, it->second.swap_graph);
2556 // The completing token need not eject its own class, so only the
2557 // positions whose chain ends in THIS class contribute to its departure.
2558 if (ps.second != cls) continue;
2559 std::vector<T> row;
2560 for (std::size_t i = 0; i < ps.first.size(); ++i)
2561 row.push_back(num_traits<T>::from_int(static_cast<long>(ps.first[i])));
2562 row.resize(W, zero);
2563 row.insert(row.end(), var.begin(), var.end());
2564 out.space.push_back(row);
2565 out.rate.push_back(ratep);
2566 out.prob.push_back(one);
2567 pas_tag_started<T>(out, it->second.svc_rate_fun, c, ps.first);
2568 }
2569 pad_tags(out);
2570 return out;
2571}
2572
2573/**
2574 * Port of `State.replyBlockInfo`: the layout of the reply block.
2575 *
2576 * The block trails the modulation, routing and shared-node columns of nvars,
2577 * occupying columns 2R+1+r. Appending is deliberate -- every existing nvars
2578 * reader keeps its indices, and the columns stay zero-width for models without
2579 * reply signals, so no other model changes state width.
2580 */
2581template <class T>
2583 const std::size_t R = sn.nclasses;
2584 ReplyBlockInfo ri;
2585 ri.slot.assign(R, static_cast<std::size_t>(-1));
2586 if (sn.nvars.size() < ind || sn.nvars[ind - 1].size() < 3 * R + 1) return ri;
2587 std::size_t pos = 0;
2588 for (std::size_t j = 0; j < 2 * R + 1; ++j) pos += sn.nvars[ind - 1][j];
2589 for (std::size_t r = 1; r <= R; ++r)
2590 if (sn.nvars[ind - 1][2 * R + r] > 0) {
2591 ri.slot[r - 1] = pos;
2592 ri.classes.push_back(r);
2593 ++pos;
2594 ++ri.width;
2595 }
2596 return ri;
2597}
2598
2599/** How many servers node `ind` is holding for pending replies, from its vars. */
2600template <class T>
2601double reply_blocked(const NetworkStruct<T>& sn, std::size_t ind, const std::vector<T>& var) {
2602 const ReplyBlockInfo ri = reply_block_info(sn, ind);
2603 double nb = 0;
2604 for (std::size_t i = 0; i < ri.classes.size(); ++i) {
2605 const std::size_t s = ri.slot[ri.classes[i] - 1];
2606 // The slot is an index into the local-variable block, which is the
2607 // TAIL of the row, so it is offset from the start of `var`.
2608 if (s != static_cast<std::size_t>(-1) && s < var.size())
2609 nb += num_traits<T>::to_double(var[s]);
2610 }
2611 return nb;
2612}
2613
2614/**
2615 * Port of `State.afterEventStationReply`: a REPLY signal completes a
2616 * synchronous call at the station holding the server for it.
2617 *
2618 * A REPLY is not a negative customer. It releases one held server and then
2619 * JOINS as an ordinary job carrying the call result onward, so unlike
2620 * NEGATIVE or CATASTROPHE it is not annihilated.
2621 *
2622 * PASS-THROUGH is the subtle part: the released server is taken by the reply
2623 * ITSELF, never by a waiting job. The reply is work this station already paid
2624 * for, so queueing it behind the residents both misreports its residence and
2625 * steals capacity. Its service is typically Immediate, so the server is handed
2626 * straight back and the ordinary departure path then promotes the head of
2627 * line -- which also keeps the occupancy within the server count, unlike
2628 * admitting the reply on top of a promoted job.
2629 */
2630template <class T>
2632 const std::vector<T>& inspace, std::size_t cls) {
2633 const std::size_t R = sn.nclasses;
2634 const std::size_t ist = sn.nodes[ind - 1].station;
2635 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2636 EventOutcome<T> out;
2637 if (ist == 0) throw InputError("after_event_station_reply: node is not a station");
2638 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2639 const ReplyBlockInfo ri = reply_block_info(sn, ind);
2640 const double S = sn.stations[ist - 1].nservers;
2641
2642 // The calling class this reply releases: the one whose expected reply IS
2643 // this class and which holds a block here.
2644 std::size_t callclass = 0;
2645 for (std::size_t i = 0; i < ri.classes.size(); ++i) {
2646 const std::size_t r = ri.classes[i];
2647 if (sn.syncreply.size() >= r && sn.syncreply[r - 1] == cls) {
2648 callclass = r;
2649 break;
2650 }
2651 }
2652
2653 std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
2654 std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
2655 std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
2656
2657 if (callclass > 0) {
2658 const std::size_t s = ri.slot[callclass - 1];
2659 if (s != static_cast<std::size_t>(-1) && s < var.size() &&
2660 num_traits<T>::to_double(var[s]) > 0)
2661 var[s] -= one;
2662 }
2663
2664 // The reply joins: into a free server, enumerating its entry phase, or at
2665 // the tail of the buffer. Servers still held for OTHER pending replies are
2666 // not available, which is what `Seff` subtracts.
2667 const double seff = S - reply_blocked(sn, ind, var);
2668 double occ = 0;
2669 for (std::size_t j = 0; j < srv.size(); ++j) occ += num_traits<T>::to_double(srv[j]);
2670 if (occ < seff) {
2671 const std::vector<T> pentry = entry_phase_dist(sn, ist, cls);
2672 for (std::size_t ke = 0; ke < L.K[cls - 1]; ++ke) {
2673 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
2674 std::vector<T> s2 = srv;
2675 s2[L.Ks[cls - 1] + ke] += one;
2676 std::vector<T> row = buf;
2677 row.insert(row.end(), s2.begin(), s2.end());
2678 row.insert(row.end(), var.begin(), var.end());
2679 out.space.push_back(row);
2680 out.rate.push_back(num_traits<T>::from_int(-1));
2681 out.prob.push_back(pentry[ke]);
2682 }
2683 return out;
2684 }
2685 // Every available server is busy: queue at the tail, which for a
2686 // right-aligned buffer is the LAST empty slot.
2687 std::vector<T> b2 = buf;
2688 std::size_t slot = b2.size();
2689 for (std::size_t b = 0; b < b2.size(); ++b)
2690 if (num_traits<T>::to_double(b2[b]) == 0) slot = b;
2691 if (slot == b2.size()) {
2692 b2.insert(b2.begin(), zero);
2693 slot = 0;
2694 }
2695 b2[slot] = num_traits<T>::from_int(static_cast<long>(cls));
2696 std::vector<T> row = b2;
2697 row.insert(row.end(), srv.begin(), srv.end());
2698 row.insert(row.end(), var.begin(), var.end());
2699 out.space.push_back(row);
2700 out.rate.push_back(num_traits<T>::from_int(-1));
2701 out.prob.push_back(one);
2702 return out;
2703}
2704
2705
2706/** Read (pos, swk, ctr) out of the local-variable block. */
2707template <class T>
2708void polling_get(const PollingInfo<T>& pi, const std::vector<T>& var, std::size_t srvclass,
2709 std::size_t& pos, std::size_t& swk, long& ctr) {
2710 pos = pi.ipos != static_cast<std::size_t>(-1)
2711 ? static_cast<std::size_t>(num_traits<T>::to_double(var[pi.off + pi.ipos]))
2712 : (srvclass > 0 ? srvclass : 1);
2713 swk = pi.iswk != static_cast<std::size_t>(-1)
2714 ? static_cast<std::size_t>(num_traits<T>::to_double(var[pi.off + pi.iswk]))
2715 : 0;
2716 ctr = pi.ictr != static_cast<std::size_t>(-1)
2717 ? static_cast<long>(num_traits<T>::to_double(var[pi.off + pi.ictr]))
2718 : 0;
2719}
2720
2721/** Write (pos, swk, ctr) back into the local-variable block. */
2722template <class T>
2723std::vector<T> polling_set(const PollingInfo<T>& pi, std::vector<T> var, std::size_t pos,
2724 std::size_t swk, long ctr) {
2725 if (pi.ipos != static_cast<std::size_t>(-1))
2726 var[pi.off + pi.ipos] = num_traits<T>::from_int(static_cast<long>(pos));
2727 if (pi.iswk != static_cast<std::size_t>(-1))
2728 var[pi.off + pi.iswk] = num_traits<T>::from_int(static_cast<long>(swk));
2729 if (pi.ictr != static_cast<std::size_t>(-1))
2730 var[pi.off + pi.ictr] = num_traits<T>::from_int(ctr);
2731 return var;
2732}
2733
2734/** Port of `State.pollingBudget`: how many services this visit may perform. */
2735template <class T>
2736long polling_budget(const PollingInfo<T>& pi, long nbufq) {
2737 switch (pi.ptype) {
2738 case lang::PollingType::EXHAUSTIVE: return 0; // unused: drains instead
2739 case lang::PollingType::GATED: return nbufq; // exactly those found
2740 case lang::PollingType::KLIMITED: return static_cast<long>(pi.pk);
2741 case lang::PollingType::DECREMENTING: return nbufq - 1;
2742 default: throw InputError("polling_budget: unsupported polling type");
2743 }
2744}
2745
2746/**
2747 * Port of `State.pollingNext`: where the server goes from buffer `pos`.
2748 *
2749 * Returns mode 1 to open a visit at q, 2 to enter the switchover into q, and 0
2750 * to PARK. Parking is reachable only with every switchover immediate, where a
2751 * server completing a full lap without finding work would otherwise cycle in
2752 * zero time forever.
2753 */
2754template <class T>
2755void polling_next(const PollingInfo<T>& pi, std::size_t pos, const std::vector<long>& nbuf,
2756 std::size_t R, bool arrived, std::size_t& q, int& mode, long& budget) {
2757 if (arrived && pi.polled[pos - 1] && nbuf[pos - 1] > 0) {
2758 // The switchover into pos is already paid for, so a visit starts here.
2759 q = pos;
2760 mode = 1;
2761 budget = polling_budget(pi, nbuf[pos - 1]);
2762 return;
2763 }
2764 std::size_t p = pos;
2765 for (std::size_t step = 0; step < R; ++step) { // a full lap, ending at pos
2766 p = p % R + 1;
2767 if (!pi.polled[p - 1]) continue;
2768 if (pi.has_sw[p - 1]) {
2769 q = p;
2770 mode = 2;
2771 budget = 0;
2772 return;
2773 }
2774 if (nbuf[p - 1] > 0) {
2775 q = p;
2776 mode = 1;
2777 budget = polling_budget(pi, nbuf[p - 1]);
2778 return;
2779 }
2780 }
2781 q = pos;
2782 mode = 0;
2783 budget = 0;
2784}
2785
2786/** Port of `State.pollingLand`: the states the walk lands in, with weights. */
2787template <class T>
2788void polling_land(const NetworkStruct<T>& sn, std::size_t ist, const PollingInfo<T>& pi,
2789 std::size_t q, int mode, long budget, const std::vector<T>& buf,
2790 const std::vector<T>& srv, const std::vector<T>& var, const RowLayout<T>& L,
2791 std::vector<std::vector<T>>& rows, std::vector<T>& probs) {
2792 const T one = num_traits<T>::from_int(1);
2793 if (mode == 1) {
2794 // Open or continue a visit at q: pull a waiting class-q job in.
2795 std::vector<T> b2 = buf;
2796 b2[q - 1] -= one;
2797 const std::vector<T> pentry = entry_phase_dist(sn, ist, q);
2798 for (std::size_t ke = 0; ke < L.K[q - 1]; ++ke) {
2799 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
2800 std::vector<T> s2 = srv;
2801 s2[L.Ks[q - 1] + ke] += one;
2802 std::vector<T> row = b2;
2803 row.insert(row.end(), s2.begin(), s2.end());
2804 const std::vector<T> v2 = polling_set(pi, var, q, 0, budget);
2805 row.insert(row.end(), v2.begin(), v2.end());
2806 rows.push_back(row);
2807 probs.push_back(pentry[ke]);
2808 }
2809 } else if (mode == 2) {
2810 // Enter the switchover into q: the facility stays EMPTY while walking.
2811 for (std::size_t ke = 0; ke < pi.ksw[q - 1]; ++ke) {
2812 if (num_traits<T>::to_double(pi.sw_pie[q - 1][ke]) <= 0) continue;
2813 std::vector<T> row = buf;
2814 row.insert(row.end(), srv.begin(), srv.end());
2815 const std::vector<T> v2 = polling_set(pi, var, q, ke + 1, 0);
2816 row.insert(row.end(), v2.begin(), v2.end());
2817 rows.push_back(row);
2818 probs.push_back(pi.sw_pie[q - 1][ke]);
2819 }
2820 } else {
2821 // Park: held until the next arrival wakes the server.
2822 std::vector<T> row = buf;
2823 row.insert(row.end(), srv.begin(), srv.end());
2824 const std::vector<T> v2 = polling_set(pi, var, q, 0, 0);
2825 row.insert(row.end(), v2.begin(), v2.end());
2826 rows.push_back(row);
2827 probs.push_back(one);
2828 }
2829}
2830
2831
2832/**
2833 * Port of the SWITCH branch: a polling server advances its switchover timer.
2834 *
2835 * Unlike PHASE, which carries only the internal transitions of a phase-type and
2836 * leaves the absorption to DEP, this event carries BOTH -- a completed
2837 * switchover moves no job, so there is no departure to attach the absorption
2838 * to. It is therefore emitted even for a single-phase switchover, where it
2839 * consists of the absorption alone.
2840 */
2841template <class T>
2843 const std::vector<T>& inspace, std::size_t cls) {
2844 const std::size_t R = sn.nclasses;
2845 const std::size_t ist = sn.nodes[ind - 1].station;
2846 const T one = num_traits<T>::from_int(1);
2847 EventOutcome<T> out;
2848 if (ist == 0) throw InputError("after_event_station_switch: node is not a station");
2849 const PollingInfo<T> pinfo = polling_info(sn, ind);
2850 if (!pinfo.valid || !pinfo.has_sw[cls - 1]) return out;
2851 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2852
2853 const std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
2854 const std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
2855 const std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
2856
2857 std::size_t pos = 0, swk = 0;
2858 long ctr = 0;
2859 polling_get(pinfo, var, 0, pos, swk, ctr);
2860 // The server must actually be inside the switchover into this buffer.
2861 if (pos != cls || swk == 0) return out;
2862
2863 // Internal transitions of the switchover phase-type.
2864 for (std::size_t kd = 0; kd < pinfo.ksw[cls - 1]; ++kd) {
2865 if (kd + 1 == swk) continue;
2866 const T r0 = pinfo.sw_d0[cls - 1](swk - 1, kd);
2867 if (num_traits<T>::to_double(r0) <= 0) continue;
2868 std::vector<T> row = buf;
2869 row.insert(row.end(), srv.begin(), srv.end());
2870 const std::vector<T> v2 = polling_set(pinfo, var, cls, kd + 1, 0);
2871 row.insert(row.end(), v2.begin(), v2.end());
2872 out.space.push_back(row);
2873 out.rate.push_back(r0);
2874 out.prob.push_back(one);
2875 }
2876 // Absorption: the server arrives and either opens a visit or walks on.
2877 T rate = num_traits<T>::from_int(0);
2878 for (std::size_t j = 0; j < pinfo.ksw[cls - 1]; ++j) rate += pinfo.sw_d1[cls - 1](swk - 1, j);
2879 if (num_traits<T>::to_double(rate) <= 0) return out;
2880 std::vector<long> nbuf(R, 0);
2881 for (std::size_t r = 0; r < R && r < L.bufw; ++r)
2882 nbuf[r] = static_cast<long>(num_traits<T>::to_double(buf[r]));
2883 std::size_t q = 0;
2884 int mode = 0;
2885 long budget = 0;
2886 polling_next(pinfo, cls, nbuf, R, true, q, mode, budget);
2887 std::vector<std::vector<T>> rows;
2888 std::vector<T> probs;
2889 polling_land(sn, ist, pinfo, q, mode, budget, buf, srv, var, L, rows, probs);
2890 for (std::size_t j = 0; j < rows.size(); ++j) {
2891 // A switchover completing over an empty buffer starts the next leg at
2892 // once, and when that leg re-enters the SAME phase of the same
2893 // switchover the landing state IS the departure state. Such a self-loop
2894 // is not a transition: emitting it would inflate the row's exit rate.
2895 if (rows[j] == inspace) continue;
2896 out.space.push_back(rows[j]);
2897 out.rate.push_back(T(rate * probs[j]));
2898 out.prob.push_back(one);
2899 // A completed switchover that opens a visit pulls a waiting class-q job
2900 // into the server, so it starts service just as an ARV or a DEP
2901 // promotion does. This is the one service start a polling station
2902 // reaches through neither, and leaving it untagged would break
2903 // startRate == TN + preemptRate there for no reason other than the name
2904 // of the carrier event.
2905 tag_last(out, mode == 1 ? q : 0, 0);
2906 }
2907 pad_tags(out);
2908 return out;
2909}
2910
2911
2912/**
2913 * Port of `State.afterEventCache`: events at a Cache node.
2914 *
2915 * A Cache is stateful but is NOT a station, so its row is [per-class counts |
2916 * cache contents | retrieval bitmap] with no buffer or server block. The
2917 * contents region holds one column per cached slot, laid out list by list;
2918 * `cpos(i,j)` is position j of list i.
2919 *
2920 * A READ is INSTANTANEOUS: every branch fires at `GlobalConstants::Immediate`,
2921 * because the read is a routing decision rather than a service. The job enters
2922 * in its read class and leaves in the hit or miss class, so the transition
2923 * both moves the job between classes and rewrites the cache contents.
2924 */
2925template <class T>
2927 const std::vector<T>& inspace, EventType event,
2928 std::size_t cls) {
2929 const std::size_t R = sn.nclasses;
2930 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2932 EventOutcome<T> out;
2933 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci = sn.nodeparam.find(ind);
2934 if (ci == sn.nodeparam.end()) throw InputError("after_event_cache: node has no CacheParam");
2935 const CacheParam<T>& cp = ci->second;
2936 const std::size_t h = cp.itemcap.size();
2937 const std::size_t n = cp.nitems;
2938 std::size_t tcc = 0; // total cache capacity, the width of the contents region
2939 for (std::size_t i = 0; i < h; ++i)
2940 if (cp.itemcap[i] > 0) tcc += static_cast<std::size_t>(cp.itemcap[i]);
2941 // cpos(i,j): position j (1-based) of list i (1-based) in the contents region.
2942 const std::vector<int>& m = cp.itemcap;
2943 struct Cpos {
2944 const std::vector<int>& m;
2945 std::size_t operator()(std::size_t i, std::size_t j) const {
2946 std::size_t base = 0;
2947 for (std::size_t t = 0; t + 1 < i; ++t) base += static_cast<std::size_t>(m[t]);
2948 return base + j - 1;
2949 }
2950 } cpos{m};
2951
2952 std::vector<T> srv(inspace.begin(), inspace.begin() + R);
2953 std::vector<T> var(inspace.begin() + R, inspace.end());
2954
2955 if (event == EventType::ARV) {
2956 srv[cls - 1] += one;
2957 std::vector<T> row = srv;
2958 row.insert(row.end(), var.begin(), var.end());
2959 out.space.push_back(row);
2960 out.rate.push_back(num_traits<T>::from_int(-1)); // passive
2961 out.prob.push_back(one);
2962 return out;
2963 }
2964
2965 if (event == EventType::DEP) {
2966 if (num_traits<T>::to_double(srv[cls - 1]) <= 0) return out;
2967 // A retrieval-class job departs the cache only to BEGIN a retrieval
2968 // (cache -> queue). Nothing in the cache state separates such an outbound job
2969 // from an inbound one, back from the retrieval stations and due to complete its
2970 // fetch on the next READ: both sit in the same per-class server slot. Left
2971 // ambiguous, the DEP and the READ syncs are both enabled and, both being
2972 // immediate, split the probability evenly, so a fetch spans a Geom(1/2) number
2973 // of station visits. Its mean is unchanged (E[N]=1), but its second moment gains
2974 // 2*E0[F]^2 and with it the delayed-hit queue length
2975 // d_i = phi_i lambda_i E0[F_i^2]/(2 E0[F_i]), which for an exponential fetch
2976 // comes out exactly twice too large. The per-item in-flight bit resolves the
2977 // ambiguity: the outbound departure below sets it and the READ that completes
2978 // the fetch clears it, so an outbound job (bit clear) may depart, while an
2979 // inbound one (bit set) may not and is left with its READ as the only action.
2980 // The block lifts as soon as another job shares the cache server, because the
2981 // READ cannot fire then either (it requires exactly one job present) and the
2982 // departure is the only way out of the state.
2983 if (cp.retrieval_capacity > 0 && !cp.retrieval_classes.empty()) {
2984 std::size_t item_dep = 0;
2985 for (std::size_t i = 0; i < cp.retrieval_classes.size() && item_dep == 0; ++i)
2986 for (std::size_t c = 0; c < cp.retrieval_classes[i].size(); ++c)
2987 if (cp.retrieval_classes[i][c] == cls) {
2988 item_dep = i + 1;
2989 break;
2990 }
2991 if (item_dep != 0) {
2992 const std::size_t bitcol = tcc + item_dep - 1;
2993 if (bitcol < var.size()) {
2994 if (num_traits<T>::to_double(var[bitcol]) != 0) {
2995 double srvtot = 0;
2996 for (std::size_t r = 0; r < R; ++r)
2997 srvtot += num_traits<T>::to_double(srv[r]);
2998 if (srvtot == 1) return out; // inbound: only its READ is enabled
2999 } else {
3000 var[bitcol] = one;
3001 }
3002 }
3003 } else {
3004 // A read class that owns retrieval classes always switches at the READ,
3005 // into a hit, a miss or a retrieval class; its cache -> station routing
3006 // exists only so that the retrieval classes can inherit it, and must
3007 // never carry the reading job itself out of the cache unread.
3008 bool owns = false;
3009 for (std::size_t i = 0; i < cp.retrieval_classes.size(); ++i)
3010 if (cls - 1 < cp.retrieval_classes[i].size() &&
3011 cp.retrieval_classes[i][cls - 1] != 0) {
3012 owns = true;
3013 break;
3014 }
3015 if (owns) {
3016 const std::size_t hc =
3017 cls - 1 < cp.hitclass.size() ? static_cast<std::size_t>(cp.hitclass[cls - 1]) : 0;
3018 const std::size_t mc =
3019 cls - 1 < cp.missclass.size() ? static_cast<std::size_t>(cp.missclass[cls - 1]) : 0;
3020 if (hc != cls && mc != cls) return out;
3021 }
3022 }
3023 }
3024 srv[cls - 1] -= one;
3025 std::vector<T> row = srv;
3026 row.insert(row.end(), var.begin(), var.end());
3027 out.space.push_back(row);
3028 out.rate.push_back(imm); // the departure is instantaneous
3029 out.prob.push_back(one);
3030 return out;
3031 }
3032
3033 if (event != EventType::READ) return out;
3034
3035 // A READ needs exactly one job present, and it must be of the reading class.
3036 double tot = 0;
3037 for (std::size_t r = 0; r < R; ++r) tot += num_traits<T>::to_double(srv[r]);
3038 if (num_traits<T>::to_double(srv[cls - 1]) <= 0 || tot != 1) return out;
3039 if (cls - 1 >= cp.pread.size() || cp.pread[cls - 1].empty()) return out;
3040 const std::vector<T>& p = cp.pread[cls - 1];
3041
3042 // The delayed-hit retrieval system. Block A (n columns) marks the items in
3043 // flight; block B (one column per retrieval class) counts the secondary
3044 // requests merged onto those fetches. Its width is read off the row rather
3045 // than off the parameters, because a struct whose space predates the block
3046 // still has to be walkable.
3047 const bool retr = cp.retrieval_capacity > 0 && !cp.retrieval_classes.empty();
3048 std::vector<std::size_t> rc_list, rc_items, rc_orig;
3049 if (retr) cache_retrieval_class_map(cp, rc_list, rc_items, rc_orig);
3050 const std::size_t block_b = tcc + n;
3051 std::size_t width_b = var.size() > block_b ? var.size() - block_b : 0;
3052 if (width_b != rc_list.size()) width_b = 0;
3053 // -1 is the sample path's unbounded merge; an exact solver enumerates block
3054 // B only up to the level it declared, and a merge past that level is refused
3055 // rather than folded into a state the space does not hold.
3056 const double maxpend = cp.max_pending_retrieval < 0
3057 ? std::numeric_limits<double>::infinity()
3058 : static_cast<double>(cp.max_pending_retrieval);
3059 bool from_retrieval = false;
3060 for (std::size_t j = 0; j < rc_list.size(); ++j)
3061 if (rc_list[j] == cls) from_retrieval = true;
3062
3063 for (std::size_t k = 1; k <= n; ++k) {
3064 if (k - 1 >= p.size() || num_traits<T>::to_double(p[k - 1]) <= 0) continue;
3065 std::vector<T> srv_e = srv;
3066 srv_e[cls - 1] -= one;
3067 // The item is searched ONLY in the contents region; the trailing
3068 // retrieval slots are a different namespace.
3069 std::size_t posk = 0;
3070 for (std::size_t c = 0; c < tcc && c < var.size(); ++c)
3071 if (static_cast<std::size_t>(num_traits<T>::to_double(var[c])) == k) {
3072 posk = c + 1;
3073 break;
3074 }
3075 // A RETURNING RETRIEVAL always completes the miss that started it, so it
3076 // takes the miss branch even where the enumeration produced a (then
3077 // unreachable) state holding item k. A retrieval class has no hit class
3078 // to switch into either.
3079 if (from_retrieval) posk = 0;
3080 const Matrix<T>& ac = cp.accost.empty() || cls - 1 >= cp.accost.size() ||
3081 k - 1 >= cp.accost[cls - 1].size()
3082 ? Matrix<T>()
3083 : cp.accost[cls - 1][k - 1];
3084 const bool have_ac = ac.rows() >= h + 1 && ac.cols() >= h + 1;
3085
3086 if (posk == 0) {
3087 // CACHE MISS, or one leg of a retrieval. A fetch is in flight iff
3088 // block A's bit for item k is set.
3089 const bool in_flight =
3090 retr && tcc + k <= var.size() && num_traits<T>::to_double(var[tcc + k - 1]) != 0;
3091 std::size_t r_class = 0;
3092 if (retr && k - 1 < cp.retrieval_classes.size() &&
3093 cls - 1 < cp.retrieval_classes[k - 1].size())
3094 r_class = cp.retrieval_classes[k - 1][cls - 1];
3095 // A retrieval-class job whose item is not recorded in the bitmap is
3096 // outbound: it has not reached the retrieval stations yet, so it has no
3097 // fetch to complete and only its departure is enabled.
3098 if (from_retrieval && !in_flight) continue;
3099
3100 if (!from_retrieval && r_class != 0) {
3101 // BEGIN a fetch, or MERGE onto one already running. The merge is
3102 // the delayed hit: it adds nothing to the cache's server block,
3103 // and is held in block B until the fetch completes and releases
3104 // it in the hit class of the class that issued it.
3105 std::vector<T> srv_b = srv_e;
3106 std::vector<T> var_b = var;
3107 if (!in_flight) {
3108 // The job switches to item k's retrieval class and waits in the cache
3109 // server for the departure that carries it to the retrieval stations.
3110 // That departure, not this READ, sets the in-flight bit, so that an
3111 // outbound retrieval job (bit clear) is distinguishable from an
3112 // inbound one (bit set); see the DEP branch above.
3113 srv_b[r_class - 1] += one;
3114 } else {
3115 if (width_b == 0) continue;
3116 std::size_t bslot = width_b;
3117 for (std::size_t j = 0; j < rc_list.size(); ++j)
3118 if (rc_list[j] == r_class) { bslot = j; break; }
3119 if (bslot >= width_b) continue;
3120 double pend = 0;
3121 for (std::size_t j = 0; j < width_b; ++j)
3122 pend += num_traits<T>::to_double(var_b[block_b + j]);
3123 if (pend >= maxpend) continue; // beyond the truncation level
3124 var_b[block_b + bslot] += one;
3125 }
3126 std::vector<T> row = srv_b;
3127 row.insert(row.end(), var_b.begin(), var_b.end());
3128 out.space.push_back(row);
3129 out.rate.push_back(T(p[k - 1] * imm));
3130 out.prob.push_back(one);
3131 continue;
3132 }
3133
3134 // The fetch is complete (or there is no retrieval system): the job
3135 // leaves in the miss class, the item may be admitted into one of the
3136 // lists, and every request merged onto this fetch is released in the
3137 // SAME transition as a delayed hit.
3138 if (cls - 1 >= cp.missclass.size() || cp.missclass[cls - 1] == 0) continue;
3139 std::vector<T> srv_m = srv_e;
3140 srv_m[cp.missclass[cls - 1] - 1] += one;
3141 std::vector<T> var_m = var;
3142 if (tcc + k <= var_m.size()) var_m[tcc + k - 1] = zero; // retrieval done
3143 for (std::size_t j = 0; j < width_b; ++j) {
3144 if (rc_items[j] != k) continue;
3145 const double held = num_traits<T>::to_double(var_m[block_b + j]);
3146 if (held <= 0) continue;
3147 const std::size_t oc = rc_orig[j];
3148 if (oc - 1 >= cp.hitclass.size() || cp.hitclass[oc - 1] == 0) continue;
3149 srv_m[cp.hitclass[oc - 1] - 1] += var_m[block_b + j];
3150 var_m[block_b + j] = zero;
3151 }
3152
3153 // Column 1 of the access cost is the REJECT branch: the item passes
3154 // through without being cached at all.
3155 const T rej = have_ac ? ac(0, 0) : zero;
3156 if (num_traits<T>::to_double(rej) > 0) {
3157 std::vector<T> row = srv_m;
3158 row.insert(row.end(), var_m.begin(), var_m.end());
3159 out.space.push_back(row);
3160 out.rate.push_back(T(rej * p[k - 1] * imm));
3161 out.prob.push_back(one);
3162 }
3163 for (std::size_t l = 1; l <= h; ++l) {
3164 const T w = have_ac ? ac(0, l) : (l == 1 ? one : zero);
3165 if (num_traits<T>::to_double(w) <= 0) continue;
3166 if (m[l - 1] <= 0) continue;
3167 const std::size_t ml = static_cast<std::size_t>(m[l - 1]);
3169 // Random replacement: the item lands uniformly in any slot.
3170 for (std::size_t rr = 1; rr <= ml; ++rr) {
3171 std::vector<T> vp = var_m;
3172 vp[cpos(l, rr)] = num_traits<T>::from_int(static_cast<long>(k));
3173 std::vector<T> row = srv_m;
3174 row.insert(row.end(), vp.begin(), vp.end());
3175 out.space.push_back(row);
3176 out.rate.push_back(T(w * p[k - 1] /
3177 num_traits<T>::from_int(static_cast<long>(ml)) * imm));
3178 out.prob.push_back(one);
3179 }
3180 } else {
3181 // The ordered families insert at the HEAD, shifting the list
3182 // down by one and evicting its tail.
3183 std::vector<T> vp = var_m;
3184 for (std::size_t j = ml; j >= 2; --j) vp[cpos(l, j)] = var_m[cpos(l, j - 1)];
3185 vp[cpos(l, 1)] = num_traits<T>::from_int(static_cast<long>(k));
3186 T rate = T(w * p[k - 1] * imm);
3187 // q-LRU admits a miss only with probability q; the rest
3188 // passes through uncached.
3190 const T q = cp.qlru;
3191 if (num_traits<T>::to_double(q) < 1) {
3192 std::vector<T> row0 = srv_m;
3193 row0.insert(row0.end(), var_m.begin(), var_m.end());
3194 out.space.push_back(row0);
3195 out.rate.push_back(T(rate * T(one - q)));
3196 out.prob.push_back(one);
3197 }
3198 rate = T(rate * q);
3199 }
3200 if (num_traits<T>::to_double(rate) <= 0) continue;
3201 std::vector<T> row = srv_m;
3202 row.insert(row.end(), vp.begin(), vp.end());
3203 out.space.push_back(row);
3204 out.rate.push_back(rate);
3205 out.prob.push_back(one);
3206 }
3207 }
3208 } else {
3209 // CACHE HIT: the job leaves in the hit class. Which list it was
3210 // found in decides how the contents move.
3211 if (cls - 1 >= cp.hitclass.size() || cp.hitclass[cls - 1] == 0) continue;
3212 std::vector<T> srv_h = srv_e;
3213 srv_h[cp.hitclass[cls - 1] - 1] += one;
3214 std::size_t li = 1, acc = 0;
3215 for (std::size_t t = 0; t < h; ++t) {
3216 const std::size_t mt = m[t] > 0 ? static_cast<std::size_t>(m[t]) : 0;
3217 if (posk <= acc + mt) { li = t + 1; break; }
3218 acc += mt;
3219 }
3220 const std::size_t j = posk - acc;
3221 if (li < h) {
3222 // A HIT BELOW THE TERMINAL LIST PROMOTES, and that is what makes
3223 // an h-list cache more than h caches side by side. Row `li` of
3224 // the access cost routes the item from list li into list
3225 // inew >= li, exactly as `afterEventCache.m` does over `inew =
3226 // i:h`; an empty accost is the reference default
3227 // `diag(ones(1,h),1)` with a 1 in the bottom-right, the linear
3228 // cache that moves the item one list up. Omitting this branch
3229 // froze every list above the first at its initial contents: on
3230 // m=[2,1] only 12 of the 60 configurations stayed reachable, and
3231 // the CTMC hit ratio of cache_compare_replc came out 4.9% low.
3232 for (std::size_t inew = li; inew <= h; ++inew) {
3233 if (m[inew - 1] <= 0) continue;
3234 const std::size_t mn = static_cast<std::size_t>(m[inew - 1]);
3235 const T w = have_ac ? ac(li, inew) : (inew == li + 1 ? one : zero);
3236 if (num_traits<T>::to_double(w) <= 0) continue;
3238 // Random replacement swaps with a uniformly drawn slot.
3239 for (std::size_t r = 1; r <= mn; ++r) {
3240 std::vector<T> vp = var;
3241 vp[cpos(li, j)] = var[cpos(inew, r)];
3242 vp[cpos(inew, r)] = num_traits<T>::from_int(static_cast<long>(k));
3243 std::vector<T> row = srv_h;
3244 row.insert(row.end(), vp.begin(), vp.end());
3245 out.space.push_back(row);
3246 out.rate.push_back(
3247 T(w * p[k - 1] /
3248 num_traits<T>::from_int(static_cast<long>(mn)) * imm));
3249 out.prob.push_back(one);
3250 }
3251 continue;
3252 }
3253 // Every read below is of the UNMODIFIED row, so the three
3254 // moves compose in the reference's order even where the
3255 // source list and the target list are the same one.
3256 std::vector<T> vp = var;
3257 // The LRU family closes the gap in list li; FIFO orders by
3258 // insertion and so leaves list li otherwise untouched.
3259 const bool ordered = cp.replacestrat != lang::ReplacementStrategy::FIFO;
3260 if (ordered)
3261 for (std::size_t t = j; t >= 2; --t) vp[cpos(li, t)] = var[cpos(li, t - 1)];
3262 // The tail evicted from the target list takes the slot the
3263 // promoted item vacated.
3264 vp[cpos(li, ordered ? 1 : j)] = var[cpos(inew, mn)];
3265 for (std::size_t t = mn; t >= 2; --t) vp[cpos(inew, t)] = var[cpos(inew, t - 1)];
3266 vp[cpos(inew, 1)] = num_traits<T>::from_int(static_cast<long>(k));
3267 std::vector<T> row = srv_h;
3268 row.insert(row.end(), vp.begin(), vp.end());
3269 out.space.push_back(row);
3270 out.rate.push_back(T(w * p[k - 1] * imm));
3271 out.prob.push_back(one);
3272 }
3276 // A hit in the terminal list does not reorder these: FIFO orders
3277 // by INSERTION, and random replacement has no order to disturb.
3278 std::vector<T> row = srv_h;
3279 row.insert(row.end(), var.begin(), var.end());
3280 out.space.push_back(row);
3281 out.rate.push_back(T(p[k - 1] * imm));
3282 out.prob.push_back(one);
3283 } else {
3284 // LRU and its relatives promote the hit item to the head of its
3285 // list, which is the whole content of "recently used".
3286 std::vector<T> vp = var;
3287 for (std::size_t t = j; t >= 2; --t) vp[cpos(li, t)] = var[cpos(li, t - 1)];
3288 vp[cpos(li, 1)] = var[cpos(li, j)];
3289 std::vector<T> row = srv_h;
3290 row.insert(row.end(), vp.begin(), vp.end());
3291 out.space.push_back(row);
3292 out.rate.push_back(T(p[k - 1] * imm));
3293 out.prob.push_back(one);
3294 }
3295 }
3296 }
3297 return out;
3298}
3299
3300
3301/** One half of a GLOBAL synchronization: a mode event at a node. */
3302template <class T>
3304 EventType event = EventType::LOCAL;
3305 std::size_t node = 0; ///< 1-based node index (a Transition, or a place)
3306 std::size_t mode = 0; ///< 1-based mode index
3307 std::size_t cls = 1; ///< 1-based class the arc moves
3308 T weight = num_traits<T>::from_int(1); ///< arc multiplicity
3309};
3310
3311/**
3312 * A GLOBAL synchronization: an SPN mode event and the place arcs it drives.
3313 *
3314 * Unlike an ordinary Sync, which pairs ONE active with ONE passive, a firing
3315 * touches every input and output place at once -- that atomicity is what makes
3316 * a Petri net transition a transition. PRE passives consume, POST produce, and
3317 * LOCAL passives are read-only (an inhibitor place, whose marking is tested but
3318 * never moved).
3319 */
3320template <class T>
3323 std::vector<ModeEvent<T>> passive;
3324};
3325
3326/**
3327 * Port of `MNetwork.refreshGlobalSync`: the ENABLE and FIRE synchronizations.
3328 *
3329 * An inhibiting place enters as a LOCAL passive rather than a PRE, and only
3330 * when it is not already an enabling or firing place: its marking is read for
3331 * the inhibition test but no token crosses the arc.
3332 */
3333template <class T>
3334std::vector<GlobalSync<T>> refresh_global_sync(const NetworkStruct<T>& sn) {
3335 std::vector<GlobalSync<T>> gsync;
3336 const T one = num_traits<T>::from_int(1);
3337 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
3338 if (sn.nodes[ind - 1].nodetype != NodeType::Transition) continue;
3339 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
3340 sn.transparam.find(ind);
3341 if (it == sn.transparam.end()) continue;
3342 const TransitionParam<T>& tp = it->second;
3343 for (int pass = 0; pass < 2; ++pass) {
3344 for (std::size_t m = 1; m <= tp.nmodes; ++m) {
3345 // ONE ENTRY PER (place, class) ARC, which is what makes a
3346 // multiclass net a different net from the class-summed one: a
3347 // Class2 token at a place must not satisfy a Class1 pre-arc, so
3348 // the pair travels into the passive rather than the place alone.
3349 std::vector<std::pair<std::size_t, std::size_t>> enab, fire, inhib;
3350 const Matrix<T>& en = tp.enabling[m - 1];
3351 const Matrix<T>& fi = tp.firing[m - 1];
3352 const Matrix<T>& ih = tp.inhibiting[m - 1];
3353 for (std::size_t q = 0; q < en.rows(); ++q)
3354 for (std::size_t r = 0; r < en.cols(); ++r)
3355 if (num_traits<T>::to_double(en(q, r)) > 0)
3356 enab.push_back(std::make_pair(q + 1, r + 1));
3357 for (std::size_t q = 0; q < fi.rows(); ++q)
3358 for (std::size_t r = 0; r < fi.cols(); ++r)
3359 if (num_traits<T>::to_double(fi(q, r)) > 0)
3360 fire.push_back(std::make_pair(q + 1, r + 1));
3361 for (std::size_t q = 0; q < ih.rows(); ++q)
3362 for (std::size_t r = 0; r < ih.cols(); ++r) {
3363 if (std::isinf(num_traits<T>::to_double(ih(q, r)))) continue;
3364 // The de-duplication is against the PLACE, not the
3365 // (place, class) pair: the passive's job is to bring the
3366 // place's marginal into the outcome, and one copy of a
3367 // place carries every class of it.
3368 bool dup = false;
3369 for (std::size_t i = 0; i < enab.size(); ++i)
3370 dup = dup || enab[i].first == q + 1;
3371 for (std::size_t i = 0; i < fire.size(); ++i)
3372 dup = dup || fire[i].first == q + 1;
3373 for (std::size_t i = 0; i < inhib.size(); ++i)
3374 dup = dup || inhib[i].first == q + 1;
3375 if (!dup) inhib.push_back(std::make_pair(q + 1, r + 1));
3376 }
3377 GlobalSync<T> g;
3378 g.active.event = pass == 0 ? EventType::ENABLE : EventType::FIRE;
3379 g.active.node = ind;
3380 g.active.mode = m;
3381 if (pass == 0) {
3382 // An ENABLE only READS the markings, so every passive is
3383 // LOCAL: enabling it is a test, not a token movement. One
3384 // per PLACE here, since a read of a place reads every class.
3385 std::vector<std::size_t> seen;
3386 auto once = [&](std::size_t q, std::size_t r) {
3387 for (std::size_t i = 0; i < seen.size(); ++i)
3388 if (seen[i] == q) return;
3389 seen.push_back(q);
3390 g.passive.push_back(ModeEvent<T>{EventType::LOCAL, q, m, r, one});
3391 };
3392 for (std::size_t i = 0; i < enab.size(); ++i) once(enab[i].first, enab[i].second);
3393 for (std::size_t i = 0; i < inhib.size(); ++i)
3394 once(inhib[i].first, inhib[i].second);
3395 } else {
3396 for (std::size_t i = 0; i < enab.size(); ++i)
3397 g.passive.push_back(ModeEvent<T>{EventType::PRE, enab[i].first, m,
3398 enab[i].second,
3399 en(enab[i].first - 1, enab[i].second - 1)});
3400 for (std::size_t i = 0; i < fire.size(); ++i)
3401 g.passive.push_back(ModeEvent<T>{EventType::POST, fire[i].first, m,
3402 fire[i].second,
3403 fi(fire[i].first - 1, fire[i].second - 1)});
3404 for (std::size_t i = 0; i < inhib.size(); ++i)
3405 g.passive.push_back(ModeEvent<T>{EventType::LOCAL, inhib[i].first, m,
3406 inhib[i].second, one});
3407 }
3408 gsync.push_back(g);
3409 }
3410 }
3411 }
3412 return gsync;
3413}
3414
3415/** What one global event produces: a whole network state per outcome. */
3416template <class T>
3418 std::vector<NetState<T>> space;
3419 std::vector<T> rate, prob;
3420 /**
3421 * True where the outcome is a firing COMPLETION, i.e. one that applied the
3422 * PRE/POST updates. Callers must NOT re-derive this from the markings: a
3423 * transition whose firing returns exactly what its enabling consumed
3424 * leaves every marking invariant yet still completed.
3425 */
3426 std::vector<bool> completion;
3427 bool empty() const { return space.empty(); }
3428};
3429
3430/**
3431 * Port of `State.afterGlobalEvent`: an SPN mode ENABLEs or FIREs.
3432 *
3433 * This is the one handler that rewrites SEVERAL nodes at once, because a
3434 * firing is atomic across all its arcs. The Transition's own row records how
3435 * many servers of each mode are idle, running (and in which firing phase), and
3436 * have just fired; the places are rewritten through the PRE and POST passives.
3437 */
3438template <class T>
3440 const GlobalSync<T>& gl) {
3441 const std::size_t R = sn.nclasses;
3442 const std::size_t ind = gl.active.node;
3443 const std::size_t mode = gl.active.mode;
3444 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
3446 GlobalOutcome<T> out;
3447 const std::size_t isf = sn.stateful_index(ind);
3448 if (isf == 0) throw InputError("after_global_event: the transition is not stateful");
3449 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
3450 sn.transparam.find(ind);
3451 if (it == sn.transparam.end()) throw InputError("after_global_event: node has no TransitionParam");
3452 const TransitionParam<T>& tp = it->second;
3453
3454 std::vector<std::size_t> fK(tp.nmodes, 1), fKs(tp.nmodes, 0);
3455 std::size_t tot = 0;
3456 for (std::size_t m = 0; m < tp.nmodes; ++m) {
3457 fK[m] = m < tp.firingphases.size() && tp.firingphases[m] > 0 ? tp.firingphases[m] : 1;
3458 fKs[m] = tot;
3459 tot += fK[m];
3460 }
3461 const std::vector<T>& row = glspace.local[isf - 1];
3462 std::vector<T> buf(row.begin(), row.begin() + tp.nmodes);
3463 std::vector<T> srv(row.begin() + tp.nmodes, row.begin() + tp.nmodes + tot);
3464 std::vector<T> fired(row.begin() + tp.nmodes + tot,
3465 row.begin() + 2 * tp.nmodes + tot);
3466 const std::vector<T> var(row.begin() + 2 * tp.nmodes + tot, row.end());
3467
3468 // The marking of every place this mode reads, node-indexed.
3469 std::vector<std::vector<T>> ep(sn.nodes.size() + 1, std::vector<T>(R, zero));
3470 for (std::size_t j = 0; j < gl.passive.size(); ++j) {
3471 const std::size_t pn = gl.passive[j].node;
3472 const std::size_t pisf = sn.stateful_index(pn);
3473 if (pisf == 0) continue;
3474 const std::pair<T, std::vector<T>> mg = to_marginal_aggr(sn, pn, glspace.local[pisf - 1]);
3475 ep[pn] = mg.second;
3476 }
3477
3478 // The enabling DEGREE: how many concurrent firings the marking supports.
3479 // An inhibitor arc disables the mode outright once its threshold is met.
3480 //
3481 // EVERY TEST IS PER (place, class), the elementwise comparison
3482 // `afterGlobalEvent.m:85` makes against `enabling_m`. Summing the marking
3483 // over classes first, as this port did until 2026-08-12, let a Class2 token
3484 // satisfy a Class1 pre-arc: on a net where Mode1 needs two Class1 tokens at
3485 // P1 and Mode2 one Class2 token there, the summed test fires Mode1 off a
3486 // marking that holds no Class1 token at all.
3487 const Matrix<T>& en_m = tp.enabling[mode - 1];
3488 const Matrix<T>& ih_m = tp.inhibiting[mode - 1];
3489 bool inhibited = false;
3490 for (std::size_t q = 0; q < ih_m.rows(); ++q)
3491 for (std::size_t r = 0; r < ih_m.cols() && r < R; ++r) {
3492 const double thr = num_traits<T>::to_double(ih_m(q, r));
3493 if (std::isinf(thr)) continue;
3494 if (num_traits<T>::to_double(ep[q + 1][r]) >= thr) inhibited = true;
3495 }
3496 bool under = false;
3497 for (std::size_t q = 0; q < en_m.rows(); ++q)
3498 for (std::size_t r = 0; r < en_m.cols() && r < R; ++r) {
3499 const double need = num_traits<T>::to_double(en_m(q, r));
3500 if (need <= 0) continue;
3501 if (num_traits<T>::to_double(ep[q + 1][r]) < need) under = true;
3502 }
3503 long mark_degree = 0;
3504 if (!inhibited && !under) {
3505 long d = 1;
3506 for (;;) {
3507 bool ok = true;
3508 for (std::size_t q = 0; q < en_m.rows() && ok; ++q)
3509 for (std::size_t r = 0; r < en_m.cols() && r < R && ok; ++r) {
3510 const double need = num_traits<T>::to_double(en_m(q, r)) * d;
3511 if (need <= 0) continue;
3512 if (num_traits<T>::to_double(ep[q + 1][r]) < need) ok = false;
3513 }
3514 if (!ok) break;
3515 ++d;
3516 }
3517 mark_degree = d - 1;
3518 }
3519 const double svm = mode - 1 < tp.nmodeservers.size() ? tp.nmodeservers[mode - 1] : 1.0;
3520 const long nsrv = std::isfinite(svm) ? static_cast<long>(svm)
3521 : static_cast<long>(GlobalConstants::MaxInt);
3522
3523 if (gl.active.event == EventType::ENABLE) {
3524 long running = 0;
3525 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3526 running += static_cast<long>(num_traits<T>::to_double(srv[fKs[mode - 1] + k]));
3527 if (inhibited || under) {
3528 // Disabled: every server of this mode returns to the idle pool.
3529 std::vector<T> b2 = buf, s2 = srv;
3530 b2[mode - 1] = num_traits<T>::from_int(nsrv);
3531 for (std::size_t k = 0; k < fK[mode - 1]; ++k) s2[fKs[mode - 1] + k] = zero;
3532 std::vector<T> nr = b2;
3533 nr.insert(nr.end(), s2.begin(), s2.end());
3534 nr.insert(nr.end(), fired.begin(), fired.end());
3535 nr.insert(nr.end(), var.begin(), var.end());
3536 if (nr == row) return out; // already disabled: not a transition
3537 NetState<T> ns = glspace;
3538 ns.local[isf - 1] = nr;
3539 out.space.push_back(ns);
3540 out.rate.push_back(imm);
3541 out.prob.push_back(one);
3542 out.completion.push_back(false);
3543 return out;
3544 }
3545 const long want = std::min(mark_degree, nsrv);
3546 if (running == want) return out; // nothing to do
3547 if (running < want) {
3548 // Start servers, distributing them over the firing phases by the
3549 // entry law; the multinomial weight is the probability of that
3550 // split.
3551 const long nadd = want - running;
3552 std::vector<T> pe(fK[mode - 1], zero);
3553 if (mode - 1 < tp.firingproc.size() && tp.firingproc[mode - 1].D0.rows() ==
3554 static_cast<std::size_t>(fK[mode - 1])) {
3555 mam::Map<T> mp;
3556 mp.D0 = tp.firingproc[mode - 1].D0;
3557 mp.D1 = tp.firingproc[mode - 1].D1;
3558 const std::vector<T> pv = mam::map_pie(mp);
3559 for (std::size_t k = 0; k < fK[mode - 1]; ++k) pe[k] = pv[k];
3560 } else {
3561 pe[0] = one;
3562 }
3563 // Enumerate the splits of nadd over the phases.
3564 std::vector<std::vector<long>> combs;
3565 std::vector<long> cur(fK[mode - 1], 0);
3566 std::function<void(std::size_t, long)> rec = [&](std::size_t k, long left) {
3567 if (k + 1 == fK[mode - 1]) {
3568 cur[k] = left;
3569 combs.push_back(cur);
3570 return;
3571 }
3572 for (long v = left; v >= 0; --v) {
3573 cur[k] = v;
3574 rec(k + 1, left - v);
3575 }
3576 };
3577 rec(0, nadd);
3578 for (std::size_t i = 0; i < combs.size(); ++i) {
3579 std::vector<T> b2 = buf, s2 = srv;
3580 b2[mode - 1] -= num_traits<T>::from_int(nadd);
3581 double logp = std::lgamma(static_cast<double>(nadd) + 1.0);
3582 bool zeroprob = false;
3583 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
3584 s2[fKs[mode - 1] + k] += num_traits<T>::from_int(combs[i][k]);
3585 const double pk = num_traits<T>::to_double(pe[k]);
3586 if (pk > 0)
3587 logp += combs[i][k] * std::log(pk) -
3588 std::lgamma(static_cast<double>(combs[i][k]) + 1.0);
3589 else if (combs[i][k] > 0)
3590 zeroprob = true;
3591 }
3592 std::vector<T> nr = b2;
3593 nr.insert(nr.end(), s2.begin(), s2.end());
3594 nr.insert(nr.end(), fired.begin(), fired.end());
3595 nr.insert(nr.end(), var.begin(), var.end());
3596 NetState<T> ns = glspace;
3597 ns.local[isf - 1] = nr;
3598 out.space.push_back(ns);
3599 out.rate.push_back(imm);
3600 out.prob.push_back(zeroprob ? zero : num_traits<T>::from_double(std::exp(logp)));
3601 out.completion.push_back(false);
3602 }
3603 return out;
3604 }
3605 // Stop the surplus servers, chosen uniformly across the phases: the
3606 // weight is the multivariate hypergeometric probability of that choice.
3607 const long ndiff = running - want;
3608 std::vector<long> sv(fK[mode - 1], 0);
3609 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3610 sv[k] = static_cast<long>(num_traits<T>::to_double(srv[fKs[mode - 1] + k]));
3611 std::vector<std::vector<long>> combs;
3612 std::vector<long> cur(fK[mode - 1], 0);
3613 std::function<void(std::size_t, long)> rec = [&](std::size_t k, long left) {
3614 if (k + 1 == fK[mode - 1]) {
3615 if (left > sv[k]) return;
3616 cur[k] = left;
3617 combs.push_back(cur);
3618 return;
3619 }
3620 for (long v = std::min(left, sv[k]); v >= 0; --v) {
3621 cur[k] = v;
3622 rec(k + 1, left - v);
3623 }
3624 };
3625 rec(0, ndiff);
3626 std::vector<double> w(combs.size(), 0.0);
3627 double wmax = -1e300;
3628 for (std::size_t i = 0; i < combs.size(); ++i) {
3629 double lw = 0;
3630 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3631 lw += std::lgamma(static_cast<double>(sv[k]) + 1.0) -
3632 std::lgamma(static_cast<double>(combs[i][k]) + 1.0) -
3633 std::lgamma(static_cast<double>(sv[k] - combs[i][k]) + 1.0);
3634 w[i] = lw;
3635 wmax = std::max(wmax, lw);
3636 }
3637 double wsum = 0;
3638 for (std::size_t i = 0; i < w.size(); ++i) {
3639 w[i] = std::exp(w[i] - wmax);
3640 wsum += w[i];
3641 }
3642 for (std::size_t i = 0; i < combs.size(); ++i) {
3643 std::vector<T> b2 = buf, s2 = srv;
3644 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3645 s2[fKs[mode - 1] + k] = num_traits<T>::from_int(sv[k] - combs[i][k]);
3646 b2[mode - 1] += num_traits<T>::from_int(ndiff);
3647 std::vector<T> nr = b2;
3648 nr.insert(nr.end(), s2.begin(), s2.end());
3649 nr.insert(nr.end(), fired.begin(), fired.end());
3650 nr.insert(nr.end(), var.begin(), var.end());
3651 NetState<T> ns = glspace;
3652 ns.local[isf - 1] = nr;
3653 out.space.push_back(ns);
3654 out.rate.push_back(imm);
3655 out.prob.push_back(num_traits<T>::from_double(wsum > 0 ? w[i] / wsum : 0.0));
3656 out.completion.push_back(false);
3657 }
3658 return out;
3659 }
3660
3661 if (gl.active.event != EventType::FIRE) return out;
3662
3663 const bool immediate_mode = mode - 1 < tp.timing.size() &&
3665 const T fw = immediate_mode && mode - 1 < tp.fireweight.size() ? tp.fireweight[mode - 1] : one;
3666 const long en_degree = inhibited ? 0 : std::min(mark_degree, nsrv);
3667 const long imm_servers = immediate_mode ? std::min(mark_degree, nsrv) : 0;
3668
3669 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
3670 const double in_k = num_traits<T>::to_double(srv[fKs[mode - 1] + k]);
3671 const bool fires = immediate_mode ? (k == 0 && imm_servers >= 1)
3672 : (in_k > 0 && en_degree >= 1);
3673 if (!fires) continue;
3674 T rate = zero;
3675 if (immediate_mode) {
3676 rate = T(imm * fw * num_traits<T>::from_int(imm_servers));
3677 } else {
3678 T d1sum = zero;
3679 if (mode - 1 < tp.firingproc.size())
3680 for (std::size_t j = 0; j < tp.firingproc[mode - 1].D1.cols(); ++j)
3681 d1sum += tp.firingproc[mode - 1].D1(k, j);
3682 rate = T(d1sum * num_traits<T>::from_double(in_k));
3683 // A marking-dependent firing rate g_mode(marking) is exact here,
3684 // because the CTMC evaluates it per enumerated state.
3685 if (mode - 1 < tp.firingdep.size() && tp.firingdep[mode - 1]) {
3686 std::vector<T> mk;
3687 for (std::size_t q = 1; q <= sn.nodes.size(); ++q) {
3688 T s2 = zero;
3689 for (std::size_t r = 0; r < R; ++r) s2 += ep[q][r];
3690 mk.push_back(s2);
3691 }
3692 rate = T(rate * tp.firingdep[mode - 1](mk));
3693 }
3694 }
3695 if (num_traits<T>::to_double(rate) <= 0) continue;
3696
3697 std::vector<T> b2 = buf, s2 = srv;
3698 if (in_k > 0) {
3699 s2[fKs[mode - 1] + k] -= one; // the firing server leaves execution
3700 b2[mode - 1] += one; // and returns to the idle pool
3701 }
3702 NetState<T> ns = glspace;
3703 std::vector<T> nr = b2;
3704 nr.insert(nr.end(), s2.begin(), s2.end());
3705 nr.insert(nr.end(), fired.begin(), fired.end());
3706 nr.insert(nr.end(), var.begin(), var.end());
3707 ns.local[isf - 1] = nr;
3708
3709 // The arcs fire ATOMICALLY with the mode: PRE consumes from every
3710 // input place and POST produces into every output place, in one
3711 // transition. Splitting them would let the net occupy a state in which
3712 // the tokens have left one place and not arrived at the other.
3713 for (std::size_t j = 0; j < gl.passive.size(); ++j) {
3714 const ModeEvent<T>& pe2 = gl.passive[j];
3715 if (pe2.event != EventType::PRE && pe2.event != EventType::POST) continue;
3716 const std::size_t pisf = sn.stateful_index(pe2.node);
3717 if (pisf == 0) continue;
3718 std::vector<T>& prow = ns.local[pisf - 1];
3719 const double wgt = num_traits<T>::to_double(pe2.weight);
3720 const std::size_t pist = sn.nodes[pe2.node - 1].station;
3721 const SchedStrategy psched =
3722 pist != 0 ? sn.stations[pist - 1].sched : SchedStrategy::INF;
3723 const std::size_t c = pe2.cls;
3724 if (pe2.event == EventType::PRE) {
3725 if (state_detail::buffer_is_class_tag(psched)) {
3726 // An ordered buffer: consume from the head end, matching
3727 // the discipline rather than a count.
3728 long left = static_cast<long>(wgt);
3729 const T tag = num_traits<T>::from_int(static_cast<long>(c));
3730 if (psched == SchedStrategy::LCFS) {
3731 for (std::size_t b = 0; b < prow.size() && left > 0; ++b)
3732 if (prow[b] == tag) { prow[b] = zero; --left; }
3733 } else {
3734 for (std::size_t b = prow.size(); b-- > 0 && left > 0;)
3735 if (prow[b] == tag) { prow[b] = zero; --left; }
3736 }
3737 } else if (prow.size() > R) {
3738 // A Place with a [count | server] split: drain the server
3739 // slot into the count, as the reference does.
3740 const double totc = num_traits<T>::to_double(prow[c - 1]) +
3741 num_traits<T>::to_double(prow[R + c - 1]);
3742 prow[c - 1] = num_traits<T>::from_double(totc - wgt);
3743 prow[R + c - 1] = zero;
3744 } else if (c - 1 < prow.size()) {
3745 prow[c - 1] -= num_traits<T>::from_double(wgt);
3746 }
3747 } else {
3748 if (state_detail::buffer_is_class_tag(psched)) {
3749 for (long q = 0; q < static_cast<long>(wgt); ++q)
3750 prow.insert(prow.begin(), num_traits<T>::from_int(static_cast<long>(c)));
3751 } else if (c - 1 < prow.size()) {
3752 prow[c - 1] += num_traits<T>::from_double(wgt);
3753 }
3754 }
3755 }
3756 out.space.push_back(ns);
3757 out.rate.push_back(rate);
3758 out.prob.push_back(one);
3759 out.completion.push_back(true);
3760 }
3761 return out;
3762}
3763
3764/** One half of a synchronization: an event at a node, in a class. */
3765template <class T>
3767 EventType event = EventType::LOCAL;
3768 std::size_t node = 0; ///< 1-based node index, or `local` for the dummy
3769 std::size_t cls = 0; ///< 1-based class index
3770 T prob = num_traits<T>::from_int(1); ///< routing probability, passive half
3771 /**
3772 * The routing probability is a FUNCTION of the network state, so `prob`
3773 * holds only the state-independent placeholder and the generator must read
3774 * `rt_state(sn, state)(rt_row, rt_col)` instead.
3775 *
3776 * The reference decides this per ACTIVE NODE (`sn.isstatedep(node_a,3)`) and
3777 * then calls whatever `prob` holds, which throws where a node routes one
3778 * class state-dependently and another by probability. Deciding it per
3779 * SYNCHRONIZATION agrees wherever the reference runs at all, and does not
3780 * throw where it would.
3781 */
3782 bool statedep = false;
3783 std::size_t rt_row = 0; ///< (isf-1)*nclasses + (r-1) of the active half
3784 std::size_t rt_col = 0; ///< (jsf-1)*nclasses + (s-1) of this passive half
3785};
3786
3787/**
3788 * One synchronization: an ACTIVE event and the PASSIVE event it drives.
3789 *
3790 * Every transition of the CTMC is one of these. The active half sets the rate;
3791 * the passive half is where the job lands, weighted by the routing probability.
3792 * A LOCAL passive half means the active event moves no job out of its node --
3793 * a phase change, a reneging job leaving the system, a server failing.
3794 */
3795template <class T>
3799
3800/**
3801 * True when a synchronization is an IMMEDIATE-FEEDBACK SELF-LOOP: a departure
3802 * whose passive half is an arrival back at the same station, in a class the
3803 * station holds its server for (`sn.immfeed`, set by
3804 * `Queue.setImmediateFeedback` or `JobClass.setImmediateFeedback`).
3805 *
3806 * The departure half of such a synchronization must run with `no_promote`, so
3807 * the vacated server is not given to a waiting job and the fed-back arrival
3808 * seizes it instead. Every walk of the synchronization list must consult this,
3809 * or it answers a model in which the self-looping job re-queues behind the
3810 * waiting jobs -- the reference's `immfeed_selfloop`.
3811 *
3812 * The class read is the PASSIVE one: after a class switch the job comes back as
3813 * the class it was switched INTO, and `refresh_sync` folds the switch into
3814 * `sn.rt`, so a class-switching self-loop is still ONE synchronization whose
3815 * passive node is its active node.
3816 */
3817template <class T>
3819 if (sy.active.event != EventType::DEP || sy.passive.node != sy.active.node) return false;
3820 if (sy.active.node == 0 || sy.active.node > sn.nodes.size()) return false;
3821 const std::size_t ist = sn.nodes[sy.active.node - 1].station;
3822 if (ist == 0) return false;
3823 const std::size_t cls_p = sy.passive.cls;
3824 if (ist > sn.immfeed.size() || cls_p == 0 || cls_p > sn.immfeed[ist - 1].size()) return false;
3825 return sn.immfeed[ist - 1][cls_p - 1];
3826}
3827
3828/**
3829 * Port of `MNetwork.refreshSync`: the synchronization list.
3830 *
3831 * The ORDER matters as much as the content: the generator adds rates in this
3832 * order, and while addition is commutative in exact arithmetic it is not in
3833 * floating point, so a reordered list perturbs the last digits of every
3834 * reported metric.
3835 *
3836 * @param impatience_classes (station x class) true where reneging is declared
3837 * @param breakdown_nodes 1-based node indices with a server breakdown
3838 * @param sn the refreshed network struct
3839 */
3840template <class T>
3841std::vector<Sync<T>> refresh_sync(
3842 const NetworkStruct<T>& sn,
3843 const std::vector<std::vector<bool>>& impatience_classes = std::vector<std::vector<bool>>(),
3844 const std::vector<std::size_t>& breakdown_nodes = std::vector<std::size_t>()) {
3845 const std::size_t R = sn.nclasses;
3846 const std::size_t local = sn.nodes.size() + 1; // the dummy passive node
3847 const T one = num_traits<T>::from_int(1);
3848 std::vector<Sync<T>> sync;
3849
3850 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
3851 const NodeDef& nd = sn.nodes[ind - 1];
3852 const std::size_t ist = nd.station;
3853 for (std::size_t r = 1; r <= R; ++r) {
3854 // A phase-change action exists only for a multi-phase service:
3855 // with one phase there is no internal transition to make.
3856 if (ist != 0 && sn.phases_of(ist, r) > 1) {
3857 Sync<T> s;
3858 s.active = SyncEvent<T>{EventType::PHASE, ind, r, one};
3859 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3860 sync.push_back(s);
3861 }
3862 if (ist != 0 && impatience_classes.size() >= ist &&
3863 impatience_classes[ist - 1].size() >= r && impatience_classes[ist - 1][r - 1]) {
3864 Sync<T> s;
3865 s.active = SyncEvent<T>{EventType::RENEGE, ind, r, one};
3866 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3867 sync.push_back(s);
3868 }
3869 if (ist != 0) {
3870 const typename std::map<std::size_t, RetrialParam<T>>::const_iterator rit =
3871 sn.retrialparam.find(ist);
3872 if (rit != sn.retrialparam.end() && rit->second.retrial_proc.size() >= r &&
3873 !rit->second.retrial_proc[r - 1].disabled) {
3874 Sync<T> s;
3875 s.active = SyncEvent<T>{EventType::RETRY, ind, r, one};
3876 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3877 sync.push_back(s);
3878 }
3879 }
3880 // Failure and repair are properties of the SERVER, not of a class,
3881 // so exactly one pair is emitted per station rather than one per
3882 // class -- hence the r == 1 guard.
3883 if (ist != 0 && r == 1) {
3884 bool has_bd = sn.has_breakdown_node(ind);
3885 for (std::size_t b = 0; b < breakdown_nodes.size() && !has_bd; ++b)
3886 if (breakdown_nodes[b] == ind) { has_bd = true; break; }
3887 if (has_bd) {
3888 Sync<T> f, rp;
3889 f.active = SyncEvent<T>{EventType::FAILURE, ind, r, one};
3890 f.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3891 sync.push_back(f);
3892 rp.active = SyncEvent<T>{EventType::REPAIR, ind, r, one};
3893 rp.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3894 sync.push_back(rp);
3895 }
3896 }
3897 // A polling station needs a SWITCH action per buffer whose
3898 // entering switchover is a real (non-immediate) walk.
3899 if (ist != 0 && sn.stations[ist - 1].sched == SchedStrategy::POLLING) {
3900 const PollingInfo<T> pinfo = polling_info(sn, ind);
3901 if (pinfo.valid && pinfo.has_sw[r - 1]) {
3902 Sync<T> s2;
3903 s2.active = SyncEvent<T>{EventType::SWITCH, ind, r, one};
3904 s2.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3905 sync.push_back(s2);
3906 }
3907 }
3908 if (!nd.stateful) continue;
3909 // A stateful Fork emits no departure sync: the atomic multi-branch
3910 // emission is a fork firing synchronization instead.
3911 if (nd.nodetype == NodeType::Fork) continue;
3912
3913 // A CACHE READ IS ITS OWN ACTION, not a routing decision. The read
3914 // consults the contents, rewrites them and switches the job into the
3915 // hit or the miss class, so it moves no job between nodes and its
3916 // passive half is the dummy.
3917 //
3918 // THE READ CLASS THEREFORE EMITS NO DEPARTURE HERE. `refresh_routing`
3919 // resolves its unresolved cache split to a uniform half-half so the
3920 // VISIT equations have a number; the reference leaves the same two
3921 // entries NaN, and `ceil(NaN) > 0` is false, so no departure sync is
3922 // built from them. Reproducing the half-half as a synchronization
3923 // would make the sample path decide hit against miss by a coin
3924 // instead of by reading the cache, which is what it did.
3925 bool cache_read_class = false;
3926 if (nd.nodetype == NodeType::Cache) {
3927 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci =
3928 sn.nodeparam.find(ind);
3929 if (ci != sn.nodeparam.end()) {
3930 if (r - 1 < ci->second.pread.size() && !ci->second.pread[r - 1].empty()) {
3931 Sync<T> s;
3932 s.active = SyncEvent<T>{EventType::READ, ind, r, one};
3933 s.passive = SyncEvent<T>{EventType::READ, local, r, one};
3934 sync.push_back(s);
3935 }
3936 cache_read_class =
3937 r - 1 < ci->second.hitclass.size() && ci->second.hitclass[r - 1] != 0;
3938 }
3939 }
3940 // A stateful Transition emits one server phase-change action per
3941 // MODE, not per class (the JAR gates the same way on the first
3942 // class); its cls slot carries the mode, as `after_event_transition`
3943 // reads it.
3944 if (nd.nodetype == NodeType::Transition && r == 1) {
3945 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator ti =
3946 sn.transparam.find(ind);
3947 if (ti != sn.transparam.end()) {
3948 for (std::size_t m = 1; m <= ti->second.nmodes; ++m) {
3949 Sync<T> s;
3950 s.active = SyncEvent<T>{EventType::PHASE, ind, m, one};
3951 s.passive = SyncEvent<T>{EventType::LOCAL, local, m, one};
3952 sync.push_back(s);
3953 }
3954 }
3955 }
3956 if (cache_read_class) continue;
3957
3958 const std::size_t isf = sn.stateful_index(ind);
3959 for (std::size_t jnd = 1; jnd <= sn.nodes.size(); ++jnd) {
3960 if (!sn.nodes[jnd - 1].stateful) continue;
3961 const std::size_t jsf = sn.stateful_index(jnd);
3962 for (std::size_t s = 1; s <= R; ++s) {
3963 const T p = sn.rt((isf - 1) * R + (r - 1), (jsf - 1) * R + (s - 1));
3964 if (num_traits<T>::to_double(p) <= 0) continue;
3965 Sync<T> ns;
3966 ns.active = SyncEvent<T>{EventType::DEP, ind, r, one};
3967 ns.passive = SyncEvent<T>{EventType::ARV, jnd, s, p};
3968 // SDR reaches `rt` as the uniform placeholder `refresh_routing`
3969 // writes, so the SUPPORT of the mask is right and the values
3970 // are not: the pair exists exactly where a link does, and the
3971 // generator replaces the probability state by state. This is
3972 // the reference's `rtmask = rtfun(emptystate, emptystate)`,
3973 // which likewise keeps every connected pair.
3974 if (nd.routing.size() >= s && nd.routing[s - 1] == RoutingStrategy::SDR) {
3975 ns.passive.statedep = true;
3976 ns.passive.rt_row = (isf - 1) * R + (r - 1);
3977 ns.passive.rt_col = (jsf - 1) * R + (s - 1);
3978 }
3979 sync.push_back(ns);
3980 }
3981 }
3982 }
3983 }
3984 return sync;
3985}
3986
3987/**
3988 * Port of `State.afterFJEvent`: fire ONE entry of the fork firing list.
3989 *
3990 * A fork firing is atomic across several nodes -- it consumes the parent at the
3991 * fork and places one sibling at each branch head in the same instant -- so
3992 * unlike every ordinary transition it cannot be decomposed into an active half
3993 * and a passive half. It therefore takes the whole network state, exactly as an
3994 * SPN global synchronization does.
3995 *
3996 * THE TWO ENABLING CONDITIONS.
3997 *
3998 * 1. The fork holds at least one class-r parent.
3999 *
4000 * 2. This entry's tag is the LOWEST FREE tag for this (fork, class). A tag is
4001 * free when its auxiliary classes have zero occupancy NETWORK-WIDE, which is
4002 * why the test scans every stateful node and not just the branches: a
4003 * sibling in transit is still outstanding. Without the canonical choice
4004 * every firing would produce one successor per free tag, all of them
4005 * relabellings of each other, and the chain would carry a factorial number
4006 * of duplicate states.
4007 *
4008 * The emission is applied SEQUENTIALLY over a growing outcome list rather than
4009 * branch-by-branch into one state, because that is what handles the three cases
4010 * a single pass would get wrong: two branches sharing a head node, `weight > 1`
4011 * repeated emissions on one branch, and a non-exponential sibling service whose
4012 * phase-entry mixture makes one arrival into several outcomes.
4013 */
4014template <class T>
4016 const NetState<T>& gl) {
4017 const std::size_t R = sn.nclasses;
4018 GlobalOutcome<T> out;
4019 const std::size_t isf_f = sn.stateful_index(e.fork);
4020 if (isf_f == 0) return out;
4021 const std::vector<T>& fs = gl.local[isf_f - 1];
4022 if (fs.size() < R) return out;
4023 if (num_traits<T>::to_double(fs[fs.size() - R + e.cls - 1]) < 1) return out;
4024
4025 // Tag occupancy, network-wide.
4026 const std::size_t B = e.auxall.size();
4027 if (B == 0 || e.tag == 0 || e.tag > e.auxall[0].size()) return out;
4028 const std::size_t Tt = e.auxall[0].size();
4029 std::vector<double> nglobal(R, 0.0);
4030 for (std::size_t isf = 1; isf <= sn.stateful_nodes.size(); ++isf) {
4031 const std::pair<T, std::vector<T>> mg =
4032 to_marginal_aggr(sn, sn.stateful_nodes[isf - 1], gl.local[isf - 1]);
4033 for (std::size_t r = 0; r < R; ++r) {
4034 const double v = num_traits<T>::to_double(mg.second[r]);
4035 // A Source encodes its reservoir as an infinite marginal; adding it
4036 // would make every tag look occupied. An auxiliary class is never
4037 // generated by a Source, so skipping the non-finite entries cannot
4038 // hide a real sibling.
4039 if (std::isfinite(v)) nglobal[r] += v;
4040 }
4041 }
4042 std::vector<double> occ(Tt, 0.0);
4043 for (std::size_t t = 0; t < Tt; ++t)
4044 for (std::size_t b = 0; b < B; ++b) occ[t] += nglobal[e.auxall[b][t] - 1];
4045 if (occ[e.tag - 1] > 0) return out; // this tag is in use
4046 for (std::size_t t = 0; t + 1 < e.tag; ++t)
4047 if (occ[t] == 0) return out; // a lower tag is free
4048
4049 NetState<T> seed = gl;
4050 seed.local[isf_f - 1][seed.local[isf_f - 1].size() - R + e.cls - 1] -=
4052
4053 std::vector<NetState<T>> partials(1, seed);
4054 std::vector<T> partprob(1, num_traits<T>::from_int(1));
4055 // The emission list, one entry per sibling. `weightlink` is filled only when
4056 // the fork sends different counts down different links; the interleave below
4057 // reproduces `repmat(1:B,1,w)` exactly in the uniform case, so a plain fork
4058 // walks the order it always did.
4059 std::vector<std::size_t> emissions;
4060 if (e.weightlink.empty()) {
4061 const std::size_t w = e.weight == 0 ? 1 : e.weight;
4062 for (std::size_t rep = 0; rep < w; ++rep)
4063 for (std::size_t b = 0; b < B; ++b) emissions.push_back(b);
4064 } else {
4065 std::size_t wmax = 0;
4066 for (std::size_t b = 0; b < e.weightlink.size(); ++b)
4067 if (e.weightlink[b] > wmax) wmax = e.weightlink[b];
4068 for (std::size_t rep = 1; rep <= wmax; ++rep)
4069 for (std::size_t b = 0; b < B; ++b)
4070 if (b < e.weightlink.size() && e.weightlink[b] >= rep) emissions.push_back(b);
4071 }
4072 for (std::size_t ei = 0; ei < emissions.size(); ++ei) {
4073 const std::size_t b = emissions[ei];
4074 const std::size_t bh = e.branchheads[b];
4075 const std::size_t isf_b = sn.stateful_index(bh);
4076 if (isf_b == 0) return GlobalOutcome<T>();
4077 const std::size_t a = e.auxclasses[b];
4078 std::vector<NetState<T>> nextp;
4079 std::vector<T> nextq;
4080 for (std::size_t pp = 0; pp < partials.size(); ++pp) {
4081 const EventOutcome<T> arv =
4082 after_event(sn, bh, partials[pp].local[isf_b - 1], EventType::ARV, a);
4083 if (arv.space.empty()) return GlobalOutcome<T>(); // blocked: no firing
4084 for (std::size_t io = 0; io < arv.space.size(); ++io) {
4085 NetState<T> ns = partials[pp];
4086 ns.local[isf_b - 1] = arv.space[io];
4087 nextp.push_back(ns);
4088 nextq.push_back(io < arv.prob.size() ? T(partprob[pp] * arv.prob[io])
4089 : partprob[pp]);
4090 }
4091 }
4092 partials.swap(nextp);
4093 partprob.swap(nextq);
4094 }
4095
4096 out.space = partials;
4097 for (std::size_t i = 0; i < partials.size(); ++i) {
4099 out.prob.push_back(T(partprob[i] * e.prob));
4100 out.completion.push_back(true);
4101 }
4102 return out;
4103}
4104
4105} // namespace qn
4106} // namespace line
4107
4108#endif // LINE_LANG_QN_STATE_EVENTS_H
Base error for the multiprecision C++ port.
Definition error.h:31
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
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:426
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:365
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:168
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:170
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:174
@ FCFS
the oldest waiting job; servers only once nobody waits
Definition lang_types.h:176
@ LCFS
the newest waiting job; servers only once nobody waits
Definition lang_types.h:177
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:175
EventType
The events a state can undergo, with the values of MATLAB EventType.
Definition lang_types.h:111
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:375
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:374
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:373
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:376
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
const char * event_to_text(EventType e)
Definition lang_types.h:137
@ QLRU
q-LRU: LRU with probabilistic admission on a miss
Definition lang_types.h:387
@ FIFO
first in, first out
Definition lang_types.h:382
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
std::vector< T > entry_phase_dist(const NetworkStruct< T > &sn, std::size_t ist, std::size_t cls)
The entry-phase distribution pie{ist}{class}: which phase a service STARTS in.
long polling_budget(const PollingInfo< T > &pi, long nbufq)
Port of State.pollingBudget: how many services this visit may perform.
EventOutcome< T > after_event_join(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Port of State.afterEventJoin: an event at a Join node of an FJ-augmented struct.
EventOutcome< T > after_event_station_pas(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Defined below; the ARV and DEP branches divert to it before any slicing.
void tag_last(EventOutcome< T > &out, std::size_t start_cls, std::size_t preempt_cls)
Tag the successor row just appended to OUT: START_CLS begins service on it and PREEMPT_CLS is displac...
void cache_retrieval_class_map(const CacheParam< T > &cp, std::vector< std::size_t > &rc_list, std::vector< std::size_t > &rc_items, std::vector< std::size_t > &rc_orig)
Port of State.cacheRetrievalClassMap: the canonical order of a cache's retrieval classes,...
std::pair< std::vector< std::size_t >, std::vector< T > > signal_batch_pmf(const NetworkStruct< T > &sn, std::size_t cls, std::size_t ntot)
Port of State.signalBatchPMF: the batch size a negative signal removes.
ReplyBlockInfo reply_block_info(const NetworkStruct< T > &sn, std::size_t ind)
Defined below; the departure branch records a server held for a reply.
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
void polling_get(const PollingInfo< T > &pi, const std::vector< T > &var, std::size_t srvclass, std::size_t &pos, std::size_t &swk, long &ctr)
Defined below; the polling branches of ARV, DEP and SWITCH use these.
EventOutcome< T > after_event_station_switch(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls)
Port of the SWITCH branch: a polling server advances its switchover timer.
void rr_advance_row(const NetworkStruct< T > &sn, std::size_t ind, std::size_t cls, std::vector< std::vector< T > > &rows)
Port of State.afterEventStation's dispatch: the successors of one event at one station.
void polling_next(const PollingInfo< T > &pi, std::size_t pos, const std::vector< long > &nbuf, std::size_t R, bool arrived, std::size_t &q, int &mode, long &budget)
Port of State.pollingNext: where the server goes from buffer pos.
bool immfeed_self_loop(const NetworkStruct< T > &sn, const Sync< T > &sy)
True when a synchronization is an IMMEDIATE-FEEDBACK SELF-LOOP: a departure whose passive half is an ...
std::vector< T > polling_set(const PollingInfo< T > &pi, std::vector< T > var, std::size_t pos, std::size_t swk, long ctr)
Write (pos, swk, ctr) back into the local-variable block.
GlobalOutcome< T > after_global_event(const NetworkStruct< T > &sn, const NetState< T > &glspace, const GlobalSync< T > &gl)
Port of State.afterGlobalEvent: an SPN mode ENABLEs or FIREs.
EventOutcome< T > after_event_station_reply(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls)
Port of State.afterEventStationReply: a REPLY signal completes a synchronous call at the station hold...
EventOutcome< T > after_event_station_arv(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls)
Port of the ARV branch of State.afterEventStation: an arriving class-cls job joins node ind,...
EventOutcome< T > after_event_station_renege(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls, const T &impatience_mu)
Port of the RENEGE branch: a WAITING class-cls job abandons the queue.
EventOutcome< T > after_event_cache(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Port of State.afterEventCache: events at a Cache node.
EventOutcome< T > after_event_station_signal(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls)
Port of State.afterEventStationSignal: a G-network signal arrives.
bool is_physical_capacity(const NetworkStruct< T > &sn, std::size_t ist, std::size_t cls)
Port of State.isPhysicalCapacity: true when the bound at (ist, class) is a PHYSICAL capacity rather t...
PrioPop< T > prio_pop(const NetworkStruct< T > &sn, std::size_t ist, const Marginal< T > &m, std::size_t cls, double ni, double S)
Compute the *PRIO effective population; a no-op for every other discipline.
T cd_factor(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &nir, std::size_t cls)
Port of State.cdclassfactor: the class-dependence multiplier of a class-cls rate at the per-class pop...
EventOutcome< T > after_event_station_phase(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls)
Port of the PHASE branch of State.afterEventStation: service advances a phase WITHOUT completing.
std::pair< std::vector< T >, std::vector< T > > phase_rates(const NetworkStruct< T > &sn, std::size_t ist, std::size_t cls)
sn.mu and sn.phi for one (station, class), derived as MATLAB's Markovian.getMu / getPhi derive them f...
EventOutcome< T > after_event_transition(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t mode)
Port of State.afterEventTransition, the PHASE arm: one running server of the given mode advances its ...
EventOutcome< T > after_event_router(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Port of State.afterEventRouter: a Router holds a job for the instant it takes to decide where it goes...
T lld_factor(const NetworkStruct< T > &sn, std::size_t ist, double n)
The limited-load-dependent multiplier at population n, 1 when unset.
void pad_tags(EventOutcome< T > &out)
Bring the tag vectors up to one entry per successor, so a caller can index them exactly like space.
void pas_tag_started(EventOutcome< T > &out, const F &mu_fun, const std::vector< std::size_t > &cold, const std::vector< std::size_t > &cnew)
Tag the successor just appended to OUT with the PAS positions that started service on it: those of CN...
std::vector< Sync< T > > refresh_sync(const NetworkStruct< T > &sn, const std::vector< std::vector< bool > > &impatience_classes=std::vector< std::vector< bool > >(), const std::vector< std::size_t > &breakdown_nodes=std::vector< std::size_t >())
Port of MNetwork.refreshSync: the synchronization list.
EventOutcome< T > after_event(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
Port of State.afterEvent: the successors of one event at one NODE.
std::pair< std::vector< std::size_t >, std::size_t > pass_and_swap(const std::vector< std::size_t > &c, std::size_t p, const std::vector< std::vector< bool > > &G)
Port of State.passAndSwap: the transition a service completion triggers at a pass-and-swap station (D...
std::pair< T, std::vector< T > > to_marginal_aggr(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &state_i)
Port of State.toMarginalAggr: the job counts of one node's state row, without the per-phase detail to...
EventOutcome< T > after_event_fork(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Port of State.afterEventFork: an event at a STATEFUL Fork node.
EventOutcome< T > after_event_station(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
std::vector< GlobalSync< T > > refresh_global_sync(const NetworkStruct< T > &sn)
Port of MNetwork.refreshGlobalSync: the ENABLE and FIRE synchronizations.
std::vector< double > pas_increments(const F &mu_fun, const std::vector< std::size_t > &c)
Port of State.afterEventStationPAS: events at a pass-and-swap station.
T service_share(const NetworkStruct< T > &sn, std::size_t ist, const Marginal< T > &m, std::size_t cls, double ni, double S)
Defined below; DEP and PHASE must share one definition of the share.
GlobalOutcome< T > after_fj_event(const NetworkStruct< T > &sn, const FjSync< T > &e, const NetState< T > &gl)
Port of State.afterFJEvent: fire ONE entry of the fork firing list.
RowLayout< T > row_layout(const NetworkStruct< T > &sn, std::size_t ind, std::size_t width)
EventOutcome< T > after_event_station_dep(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls, bool no_promote=false)
Port of the DEP branch of State.afterEventStation: a class-cls job completes service at station ind.
PollingInfo< T > polling_info(const NetworkStruct< T > &sn, std::size_t ind)
EventOutcome< T > after_event_station_breakdown(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, bool up, const T &mu)
Port of the FAILURE and REPAIR branches: the server goes down, or comes back.
bool arrival_is_lost(const NetworkStruct< T > &sn, std::size_t ist, std::size_t cls)
Port of State.arrivalIsLost: true when an arrival that finds no room is LOST, false when it must BLOC...
EventOutcome< T > after_event_station_retry(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, std::size_t cls, const T &retrial_mu, bool constant_policy=false)
Port of the RETRY branch: an ORBITING class-cls job retries entry.
void polling_land(const NetworkStruct< T > &sn, std::size_t ist, const PollingInfo< T > &pi, std::size_t q, int mode, long budget, const std::vector< T > &buf, const std::vector< T > &srv, const std::vector< T > &var, const RowLayout< T > &L, std::vector< std::vector< T > > &rows, std::vector< T > &probs)
Port of State.pollingLand: the states the walk lands in, with weights.
double reply_blocked(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &var)
Defined below; the reply block subtracts held servers in the ARV branch.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
State.pollingInfo and the controller description it returns.
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:853
std::vector< Matrix< T > > Dmark
MMAP per-class D1 blocks / BMAP per-batch-size blocks; empty otherwise.
Definition lang_types.h:863
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:766
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Server breakdown and repair of a station whose server fails and is repaired.
T failure_rate
sn.breakdownMu: 1 / mean failure time
T repair_rate
sn.repairMu: 1 / mean repair time
T qlru
Delayed-hit retrieval system (Cache.setRetrievalSystem).
std::vector< std::vector< Matrix< T > > > accost
(u) x (n) of (h+1)x(h+1), or empty
long max_pending_retrieval
Truncation level of block B: how many secondary requests may be merged onto the in-flight fetches of ...
std::vector< std::vector< std::size_t > > retrieval_classes
(nitems x nclasses), 1-based
std::vector< int > itemcap
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
lang::ReplacementStrategy replacestrat
std::vector< std::vector< T > > pread
(u) x (n), empty row = NaN
What one event produces at one node: the successor rows, their rates and their probabilities,...
std::vector< T > prob
per-row probability of the choice
std::vector< std::vector< T > > space
successor local state rows
std::vector< T > rate
per-row rate, -1 on a passive half
std::vector< std::vector< std::size_t > > start
START annotation: the 1-based classes that BEGIN or RESUME holding a server on each successor row.
std::vector< std::vector< std::size_t > > preempt
PREEMPT annotation: the 1-based classes pushed back into the buffer.
sn.nodeparam{j}.fj: what a Join node needs to fire on identity.
std::vector< std::size_t > origclasses
std::map< std::size_t, std::vector< std::vector< std::size_t > > > auxmatrix
std::map< std::size_t, std::vector< std::size_t > > required
One fork firing synchronization: sn.fjsync{k}.
std::size_t fork
1-based Fork node
std::vector< std::size_t > weightlink
Per-branch tasksPerLink, EMPTY when every branch carries weight.
std::size_t weight
tasksPerLink: siblings emitted per branch
std::vector< std::size_t > branchheads
1-based node per branch
std::size_t cls
1-based ORIGINAL class being forked
std::vector< std::vector< std::size_t > > auxall
(B x T) every auxiliary class of this (fork, class), for the tag scan.
std::vector< std::size_t > auxclasses
the tag's auxiliary class per branch
std::size_t tag
1-based tag this entry allocates
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:766
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:771
What one global event produces: a whole network state per outcome.
std::vector< T > rate
std::vector< NetState< T > > space
std::vector< T > prob
std::vector< bool > completion
True where the outcome is a firing COMPLETION, i.e.
A GLOBAL synchronization: an SPN mode event and the place arcs it drives.
ModeEvent< T > active
std::vector< ModeEvent< T > > passive
What State.toMarginal returns for one station and one state row.
Definition state.h:51
std::vector< std::vector< T > > kir
jobs in service per class and phase
Definition state.h:55
std::vector< T > nir
jobs per class
Definition state.h:53
std::vector< T > sir
jobs in service per class
Definition state.h:54
One half of a GLOBAL synchronization: a mode event at a node.
std::size_t mode
1-based mode index
std::size_t node
1-based node index (a Transition, or a place)
T weight
arc multiplicity
std::size_t cls
1-based class the arc moves
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2157
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2158
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
Port of State.pollingInfo: the derived description of a polling controller.
std::vector< bool > has_sw
std::vector< std::size_t > ksw
std::vector< std::vector< T > > sw_pie
std::vector< Matrix< T > > sw_d0
std::vector< Matrix< T > > sw_d1
std::vector< bool > polled
lang::PollingType ptype
The population the *PRIO disciplines actually share the server among.
std::vector< T > nir
nirprio when masked, the plain marginal otherwise
bool served
false when cls is not in the most urgent group
double ni
niprio when masked, the plain total otherwise
bool masked
whether the saturated-station mask was applied
Where node ind keeps its reply-block counters inside the local vars.
std::vector< std::size_t > slot
slot[r-1] = 0-based column, or npos
std::vector< std::size_t > classes
1-based calling classes holding a block
How a station's state row splits into [buffer | server | local vars].
std::size_t srvw
total server width
std::size_t bufw
buffer width, the only discipline-dependent part
std::vector< std::size_t > Ks
offset of class r's phase block
std::size_t nvar
local-variable width
std::vector< std::size_t > K
phases per class
One station of the network.
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
One half of a synchronization: an event at a node, in a class.
std::size_t cls
1-based class index
T prob
routing probability, passive half
std::size_t rt_row
(isf-1)*nclasses + (r-1) of the active half
std::size_t rt_col
(jsf-1)*nclasses + (s-1) of this passive half
std::size_t node
1-based node index, or local for the dummy
bool statedep
The routing probability is a FUNCTION of the network state, so prob holds only the state-independent ...
One synchronization: an ACTIVE event and the PASSIVE event it drives.
SyncEvent< T > passive
SyncEvent< T > active
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< T > fireweight
weight among simultaneously enabled modes
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.
std::vector< std::function< T(const std::vector< T > &)> > firingdep
Marking-dependent firing-rate multiplier g_m(marking); an empty entry is the unit multiplier.
std::vector< Matrix< T > > inhibiting
inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never).
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian