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 // PR resumes the victim in the phase it held; PI restarts it
657 // from the entry phase. That single value is the whole
658 // difference between the two families in this branch.
659 const bool resume = sched == SchedStrategy::LCFSPR ||
660 sched == SchedStrategy::LCFSPRPRIO ||
661 sched == SchedStrategy::FCFSPR ||
662 sched == SchedStrategy::FCFSPRPRIO;
663 bool can_preempt_any = false;
664 for (std::size_t cp = 1; cp <= R; ++cp) {
665 if (prio_aware) {
666 // Across priority groups a strictly higher-priority
667 // arrival preempts. WITHIN a group the base discipline
668 // decides: LCFS-PR keeps the NEWEST job in service, so
669 // an equal-priority arrival preempts; FCFS-PR never
670 // lets an arrival preempt.
671 const int pa = sn.classes[cls - 1].prio;
672 const int pv = sn.classes[cp - 1].prio;
673 if (lcfs_family ? (pa > pv) : (pa >= pv)) continue;
674 }
675 for (std::size_t pp = 0; pp < L.K[cp - 1]; ++pp) {
676 const std::size_t vcol = L.Ks[cp - 1] + pp;
677 const double busy = num_traits<T>::to_double(srv[vcol]);
678 if (busy <= 0) continue;
679 can_preempt_any = true;
680 std::vector<T> b2 = buf, s2 = srv;
681 s2[vcol] -= one;
682 s2[L.Ks[cls - 1] + kentry] += one;
683 // Rightmost empty PAIR, which is where a displaced job
684 // is stored; the class column is one left of the zero
685 // the scan finds.
686 std::size_t slot = 0;
687 for (std::size_t b = 0; b < L.bufw; ++b)
688 if (num_traits<T>::to_double(b2[b]) == 0) slot = b;
689 if (slot == 0) continue; // no room to hold the victim
690 b2[slot - 1] = num_traits<T>::from_int(static_cast<long>(cp));
691 b2[slot] = resume ? num_traits<T>::from_int(static_cast<long>(pp + 1))
692 : one;
693 std::vector<T> row = b2;
694 row.insert(row.end(), s2.begin(), s2.end());
695 row.insert(row.end(), var.begin(), var.end());
696 cand.push_back(row);
697 // the displaced job leaves the server and the arriving
698 // one takes it, on the same arc
699 cand_start.push_back(cls);
700 cand_preempt.push_back(cp);
701 // The victim is drawn uniformly among the jobs in
702 // service, so its share of the occupancy weights the
703 // successor.
704 cand_prob.push_back(T(pentry[kentry] * num_traits<T>::from_double(busy / occ)));
705 }
706 }
707 // Every busy server holds a job this arrival may not displace,
708 // so the job WAITS instead. Without this the loop above emits
709 // nothing, the arrival transition does not exist at all, and
710 // the class can never enter a busy station -- its queue is
711 // then silently understated. It is stored as a (class, entry
712 // phase) pair exactly as a preempted job is, so promotion
713 // resumes it from that phase.
714 if (prio_aware && !can_preempt_any) {
715 std::size_t slot = 0;
716 for (std::size_t b = 0; b < L.bufw; ++b)
717 if (num_traits<T>::to_double(buf[b]) == 0) slot = b;
718 if (slot > 0) {
719 std::vector<T> b2 = buf;
720 b2[slot - 1] = num_traits<T>::from_int(static_cast<long>(cls));
721 b2[slot] = num_traits<T>::from_int(static_cast<long>(kentry + 1));
722 std::vector<T> row = b2;
723 row.insert(row.end(), srv.begin(), srv.end());
724 row.insert(row.end(), var.begin(), var.end());
725 cand.push_back(row);
726 cand_prob.push_back(pentry[kentry]);
727 cand_start.push_back(0); // it preempts nothing and waits
728 cand_preempt.push_back(0);
729 }
730 }
731 }
732 } else {
733 throw UnsupportedError(
734 std::string("after_event_station_arv: the ") + lang::sched_to_text(sched) +
735 " discipline is not ported yet");
736 }
737
738 // The capacity filter of the reference: drop any successor that would
739 // exceed the station or class bound.
740 for (std::size_t c = 0; c < cand.size(); ++c) {
741 const std::pair<T, std::vector<T>> og = to_marginal_aggr(sn, ind, cand[c]);
742 if (num_traits<T>::to_double(og.second[cls - 1]) > ccap) continue;
743 if (num_traits<T>::to_double(og.first) > cap_i) continue;
744 out.space.push_back(cand[c]);
745 out.rate.push_back(minus_one);
746 out.prob.push_back(cand_prob[c]);
747 // the capacity gate drops rows, so the tags are attached here
748 tag_last(out, c < cand_start.size() ? cand_start[c] : 0,
749 c < cand_preempt.size() ? cand_preempt[c] : 0);
750 }
751 }
752 pad_tags(out);
753 return out;
754}
755
756/**
757 * `sn.mu` and `sn.phi` for one (station, class), derived as MATLAB's
758 * `Markovian.getMu` / `getPhi` derive them from the (D0, D1) pair:
759 *
760 * mu(k) = -D0(k,k) total exit rate of phase k
761 * phi(k) = sum_j D1(k,j) / -D0(k,k) probability the exit COMPLETES service
762 *
763 * so mu*phi is the departure rate and mu*(1-phi) the phase-advance rate. An
764 * Immediate process has D0(1,1) = 0 and takes phi = 1, since every exit of a
765 * zero-duration service is a completion.
766 */
767template <class T>
768std::pair<std::vector<T>, std::vector<T>> phase_rates(const NetworkStruct<T>& sn,
769 std::size_t ist, std::size_t cls) {
770 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
771 const std::size_t n = sn.phases_of(ist, cls);
772 std::vector<T> mu(n, num_traits<T>::from_int(0)), phi(n, num_traits<T>::from_int(1));
773 if (d.D0.rows() != n || d.D1.rows() != n) return std::make_pair(mu, phi);
774 for (std::size_t k = 0; k < n; ++k) {
775 const T dk = T(-d.D0(k, k));
776 mu[k] = dk;
777 if (num_traits<T>::to_double(dk) == 0) {
778 phi[k] = num_traits<T>::from_int(1); // Immediate: every exit completes
779 continue;
780 }
782 for (std::size_t j = 0; j < n; ++j) s += d.D1(k, j);
783 phi[k] = T(s / dk);
784 }
785 return std::make_pair(mu, phi);
786}
787
788/** The limited-load-dependent multiplier at population `n`, 1 when unset. */
789template <class T>
790T lld_factor(const NetworkStruct<T>& sn, std::size_t ist, double n) {
791 const std::vector<T>& s = sn.stations[ist - 1].lldscaling;
792 if (s.empty()) return num_traits<T>::from_int(1);
793 // Beyond the declared levels the scaling holds at its last value, which is
794 // what "limited" load-dependence means: the curve is flat past the limit.
795 if (!std::isfinite(n) || n >= static_cast<double>(s.size())) return s.back();
796 if (n < 1) return num_traits<T>::from_int(1);
797 return s[static_cast<std::size_t>(n) - 1];
798}
799
800/**
801 * Port of `State.cdclassfactor`: the class-dependence multiplier of a class-`cls`
802 * rate at the per-class population `nir`.
803 *
804 * `cdscaling` maps a 1 x R population vector to the R dimensionless scalings
805 * beta_r(n); the component of the class whose service is firing is the factor.
806 * A handle returning a SCALAR is the neutral case and yields 1 for every class,
807 * which is why the read is clamped to the vector's last entry rather than
808 * indexed blindly -- the reference's `v(min(class, numel(v)))`.
809 *
810 * `jdscaling` is folded in multiplicatively here, exactly as
811 * `State.afterEventInit` folds eta_i into the effective per-station handle.
812 * Doing it at the point of use rather than by rewriting the struct keeps the two
813 * fields distinguishable for the product-form tests elsewhere.
814 */
815template <class T>
816T cd_factor(const NetworkStruct<T>& sn, std::size_t ist, const std::vector<T>& nir,
817 std::size_t cls) {
818 const T one = num_traits<T>::from_int(1);
819 const Station<T>& st = sn.stations[ist - 1];
820 if (!st.cdscaling && !st.jdscaling) return one;
821 T f = one;
822 const CdScaling<T>* handles[2] = {&st.cdscaling, &st.jdscaling};
823 for (std::size_t h = 0; h < 2; ++h) {
824 const CdScaling<T>& fun = *handles[h];
825 if (!fun) continue;
826 const std::vector<T> v = fun(nir);
827 if (v.empty())
828 throw InputError(
829 "cd_factor: the class-dependence map returned an empty scaling vector");
830 f = T(f * v[std::min(cls, v.size()) - 1]);
831 }
832 return f;
833}
834
835/**
836 * The population the *PRIO disciplines actually share the server among.
837 *
838 * While every job fits in a server nobody is waiting and precedence is moot, so
839 * this is the plain marginal. Once the station saturates only the MOST URGENT
840 * group present is served (a LOWER `classprio` value is more urgent in LINE),
841 * and both the share and the load-dependent lookup are taken over that group
842 * alone -- `nirprio` / `niprio` in `State.afterEventStation`.
843 */
844template <class T>
845struct PrioPop {
846 std::vector<T> nir; ///< `nirprio` when masked, the plain marginal otherwise
847 double ni = 0; ///< `niprio` when masked, the plain total otherwise
848 bool masked = false; ///< whether the saturated-station mask was applied
849 bool served = true; ///< false when `cls` is not in the most urgent group
850};
851
852/** Compute the *PRIO effective population; a no-op for every other discipline. */
853template <class T>
854PrioPop<T> prio_pop(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
855 std::size_t cls, double ni, double S) {
856 const std::size_t R = sn.nclasses;
857 const SchedStrategy sched = sn.stations[ist - 1].sched;
858 PrioPop<T> p;
859 p.nir = m.nir;
860 p.ni = ni;
861 const bool prio_aware = sched == SchedStrategy::PSPRIO ||
862 sched == SchedStrategy::DPSPRIO ||
863 sched == SchedStrategy::GPSPRIO;
864 if (!prio_aware || ni <= S) return p;
865 double best = std::numeric_limits<double>::infinity();
866 for (std::size_t r = 0; r < R; ++r)
867 if (num_traits<T>::to_double(m.nir[r]) > 0)
868 best = std::min(best, static_cast<double>(sn.classes[r].prio));
869 if (static_cast<double>(sn.classes[cls - 1].prio) != best) {
870 p.served = false;
871 return p;
872 }
873 p.masked = true;
874 p.ni = 0;
875 for (std::size_t r = 0; r < R; ++r) {
876 if (static_cast<double>(sn.classes[r].prio) != best)
879 }
880 return p;
881}
882
883/** Defined below; DEP and PHASE must share one definition of the share. */
884template <class T>
885T service_share(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
886 std::size_t cls, double ni, double S);
887
888/**
889 * Port of the DEP branch of `State.afterEventStation`: a class-`cls` job
890 * completes service at station `ind`.
891 *
892 * This is the ACTIVE half, so unlike ARV it carries real rates, and the rate is
893 * where the disciplines actually differ -- the state update is nearly the same
894 * for all of them. Two rate conventions appear, and they are not
895 * interchangeable:
896 *
897 * mu(k)*phi(k)*kir -- the processor-sharing family, where the completion
898 * rate is the phase rate times the share of the server
899 * D1(k,kdest)*kir -- the FCFS family, where the MAP matrix already encodes
900 * both the completion and the phase the NEXT service
901 * starts in, so the destination phase is enumerated
902 */
903template <class T>
905 const std::vector<T>& inspace, std::size_t cls,
906 bool no_promote = false) {
907 const std::size_t R = sn.nclasses;
908 const std::size_t ist = sn.nodes[ind - 1].station;
909 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
910 EventOutcome<T> out;
911 if (ist == 0) throw InputError("after_event_station_dep: node is not a station");
912 const SchedStrategy sched = sn.stations[ist - 1].sched;
913 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI)
914 return after_event_station_pas(sn, ind, inspace, EventType::DEP, cls);
915 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
916 const double S = sn.stations[ist - 1].nservers;
917
918 std::vector<std::size_t> ph(R, 1), shift(R, 0);
919 for (std::size_t r = 0; r < R; ++r) {
920 ph[r] = L.K[r];
921 shift[r] = L.Ks[r];
922 }
923 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
924 if (num_traits<T>::to_double(m.sir[cls - 1]) <= 0) return out; // nothing to depart
925
926 const std::pair<std::vector<T>, std::vector<T>> mp = phase_rates(sn, ist, cls);
927 const std::vector<T>& mu = mp.first;
928 const std::vector<T>& phi = mp.second;
929 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
930
931 double ni = 0;
932 for (std::size_t r = 0; r < R; ++r) ni += num_traits<T>::to_double(m.nir[r]);
933 // Both dependence factors multiply every rate this branch emits, so they are
934 // folded into ONE scalar named `lld` for the arithmetic below. Their
935 // ARGUMENTS differ at a saturated *PRIO station, and the reference is not
936 // uniform about it: the load-dependent lookup takes the priority-masked
937 // total `niprio` for all three *PRIO disciplines, while the class-dependence
938 // handle takes the masked vector `nirprio` only for DPSPRIO and GPSPRIO and
939 // the plain marginal for PSPRIO (afterEventStation.m:709-711 vs :746-748 and
940 // :808-810). That asymmetry is reproduced rather than smoothed: smoothing it
941 // would move every reported metric on a model that has both.
942 const PrioPop<T> pp = prio_pop(sn, ist, m, cls, ni, S);
943 const bool cd_takes_prio =
944 pp.masked && (sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO);
945 const T lld = T(lld_factor(sn, ist, pp.ni) *
946 cd_factor(sn, ist, cd_takes_prio ? pp.nir : m.nir, cls));
947
948 // A retrial station does NOT promote from the orbit on completion: an
949 // orbiting job re-enters only through a RETRY at the retrial rate. The
950 // same suppression serves an immediate-feedback self-loop, where the
951 // departing job holds the server for its own re-arrival.
952 bool suppress_promote = no_promote;
953 {
954 const typename std::map<std::size_t, RetrialParam<T>>::const_iterator rit =
955 sn.retrialparam.find(ist);
956 if (rit != sn.retrialparam.end())
957 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r)
958 if (!rit->second.retrial_proc[r].disabled) { suppress_promote = true; break; }
959 }
960
961 for (std::size_t k = 0; k < L.K[cls - 1]; ++k) {
962 std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
963 std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
964 std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
965 const std::size_t col = L.Ks[cls - 1] + k;
966 if (num_traits<T>::to_double(srv[col]) <= 0) continue;
967 const T kir = m.kir[cls - 1][k];
968
969 if (sched == SchedStrategy::EXT) {
970 // A Source EMITS an arrival. Its reservoir is unbounded, so no job
971 // count changes; what moves is the MODULATING PHASE, from k to
972 // kentry at rate D1(k, kentry). For an exponential source that is
973 // the single entry lambda and the state is unchanged, which is why
974 // the arrival stream is memoryless; for a MAP it is exactly the
975 // correlation the source is there to produce.
976 if (!std::isfinite(sn.njobs()[cls - 1])) {
977 for (std::size_t ke = 0; ke < L.K[cls - 1]; ++ke) {
978 const T arv = d.D1(k, ke);
979 if (num_traits<T>::to_double(arv) <= 0) continue;
980 std::vector<T> row = inspace;
981 row[L.bufw + L.Ks[cls - 1] + k] -= one;
982 row[L.bufw + L.Ks[cls - 1] + ke] += one;
983 out.space.push_back(row);
984 out.rate.push_back(T(lld * arv));
985 out.prob.push_back(one);
986 }
987 }
988 } else if (sched == SchedStrategy::INF || sched == SchedStrategy::PS ||
989 sched == SchedStrategy::LPS || sched == SchedStrategy::DPS ||
990 sched == SchedStrategy::GPS || sched == SchedStrategy::PSPRIO ||
991 sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO) {
992 srv[col] -= one;
993 // The same share PHASE uses: a completion and an internal phase
994 // advance are driven by the identical fraction of the server, so
995 // they must never be computed two different ways.
996 const T rate = T(mu[k] * phi[k] * kir * service_share(sn, ist, m, cls, ni, S));
997 std::vector<T> row = buf;
998 row.insert(row.end(), srv.begin(), srv.end());
999 row.insert(row.end(), var.begin(), var.end());
1000 out.space.push_back(row);
1001 out.rate.push_back(T(lld * rate));
1002 out.prob.push_back(one);
1003 } else if (state_detail::buffer_is_class_tag(sched) && sched != SchedStrategy::FCFS) {
1004 // HOL, LCFS and LCFSPRIO share FCFS's ordered buffer but NOT its
1005 // rate convention: the completion rate is mu*phi*kir, summed over
1006 // destination phases rather than enumerated. What separates the
1007 // three is only WHICH waiting job is promoted.
1008 const bool has_waiting = ni > S && !suppress_promote;
1009 const T rate = T(mu[k] * phi[k] * kir);
1010 srv[col] -= one;
1011 if (!has_waiting) {
1012 std::vector<T> row = buf;
1013 row.insert(row.end(), srv.begin(), srv.end());
1014 row.insert(row.end(), var.begin(), var.end());
1015 out.space.push_back(row);
1016 out.rate.push_back(T(lld * rate));
1017 out.prob.push_back(one);
1018 continue;
1019 }
1020 // Position of the job that starts service, 0-based; L.bufw = none.
1021 std::size_t pos = L.bufw;
1022 if (sched == SchedStrategy::LCFS) {
1023 // Plain LCFS is NOT priority-aware: it always promotes the most
1024 // recent arrival. Arrivals fill the rightmost empty slot, so
1025 // the newest job is the FIRST nonzero column. Branching here on
1026 // the class priorities would silently turn every LCFS station
1027 // with distinct priorities into an LCFSPRIO one.
1028 for (std::size_t b = 0; b < L.bufw; ++b)
1029 if (num_traits<T>::to_double(buf[b]) != 0) { pos = b; break; }
1030 } else {
1031 // HOL and LCFSPRIO serve the highest-priority waiting group
1032 // first; in LINE a LOWER classprio value is more urgent. Within
1033 // the group HOL takes the oldest job (rightmost) and LCFSPRIO
1034 // the newest (leftmost), which is how each relates to its
1035 // non-priority base discipline.
1036 double best = std::numeric_limits<double>::infinity();
1037 for (std::size_t b = 0; b < L.bufw; ++b) {
1038 const double v = num_traits<T>::to_double(buf[b]);
1039 if (v <= 0) continue;
1040 const double p = sn.classes[static_cast<std::size_t>(v) - 1].prio;
1041 if (p < best) best = p;
1042 }
1043 if (std::isfinite(best))
1044 for (std::size_t b = 0; b < L.bufw; ++b) {
1045 const double v = num_traits<T>::to_double(buf[b]);
1046 if (v <= 0) continue;
1047 if (sn.classes[static_cast<std::size_t>(v) - 1].prio != best) continue;
1048 pos = b;
1049 if (sched == SchedStrategy::LCFSPRIO) break; // leftmost
1050 }
1051 }
1052 if (pos == L.bufw) continue;
1053 const std::size_t hc = static_cast<std::size_t>(num_traits<T>::to_double(buf[pos]));
1054 std::vector<T> b2 = buf;
1055 if (sched == SchedStrategy::LCFS) {
1056 // LCFS clears the slot IN PLACE, leaving a hole; the next
1057 // arrival refills it, since arrivals seek the rightmost empty
1058 // slot. The priority variants instead close the gap.
1059 b2[pos] = zero;
1060 } else {
1061 for (std::size_t b = pos; b > 0; --b) b2[b] = b2[b - 1];
1062 b2[0] = zero;
1063 }
1064 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1065 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1066 std::vector<T> s3 = srv;
1067 s3[L.Ks[hc - 1] + ke] += one;
1068 std::vector<T> row = b2;
1069 row.insert(row.end(), s3.begin(), s3.end());
1070 row.insert(row.end(), var.begin(), var.end());
1071 out.space.push_back(row);
1072 out.rate.push_back(T(lld * rate * pentry[ke]));
1073 out.prob.push_back(one);
1074 tag_last(out, hc, 0); // the promoted job takes the freed server
1075 }
1076 } else if (state_detail::buffer_is_class_tag(sched)) {
1077 // FCFS. D1(k, kdest) is the completion rate that leaves the process
1078 // in phase kdest, so the destination phase has to be enumerated
1079 // rather than summed away.
1080 //
1081 // A synchronous call: this departing job KEEPS its server until its
1082 // REPLY returns here, so the server is not handed to a waiting job;
1083 // it is recorded as held in the reply block instead. Servers already
1084 // held that way are likewise unavailable, so a job can be waiting
1085 // while the raw occupancy is below the server count.
1086 std::vector<T> var2 = var;
1087 bool holds_reply = false;
1088 if (sn.replyblock.size() >= ind && sn.replyblock[ind - 1].size() >= cls &&
1089 sn.replyblock[ind - 1][cls - 1]) {
1090 const ReplyBlockInfo ri = reply_block_info(sn, ind);
1091 const std::size_t sl = ri.slot[cls - 1];
1092 if (sl != static_cast<std::size_t>(-1) && sl < var2.size()) {
1093 var2[sl] += one;
1094 holds_reply = true;
1095 }
1096 }
1097 const double nb = reply_blocked(sn, ind, var);
1098 const bool has_waiting = ni > (S - nb) && !suppress_promote && !holds_reply;
1099 for (std::size_t kd = 0; kd < L.K[cls - 1]; ++kd) {
1100 const T rate = T(d.D1(k, kd) * kir);
1101 std::vector<T> s2 = srv;
1102 s2[col] -= one;
1103 if (!has_waiting) {
1104 std::vector<T> row = buf;
1105 row.insert(row.end(), s2.begin(), s2.end());
1106 row.insert(row.end(), var2.begin(), var2.end());
1107 out.space.push_back(row);
1108 out.rate.push_back(T(lld * rate));
1109 out.prob.push_back(one);
1110 continue;
1111 }
1112 // Promote the head of the buffer. The head is the LAST column:
1113 // the buffer shifts RIGHT as jobs join, so the oldest job sits
1114 // at the end -- which is what makes the discipline first-come.
1115 const double headv = num_traits<T>::to_double(buf[L.bufw - 1]);
1116 if (headv <= 0) continue;
1117 const std::size_t hc = static_cast<std::size_t>(headv);
1118 std::vector<T> b2(L.bufw, zero);
1119 for (std::size_t b = 1; b < L.bufw; ++b) b2[b] = buf[b - 1];
1120 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1121 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1122 std::vector<T> s3 = s2;
1123 s3[L.Ks[hc - 1] + ke] += one;
1124 std::vector<T> row = b2;
1125 row.insert(row.end(), s3.begin(), s3.end());
1126 row.insert(row.end(), var2.begin(), var2.end());
1127 const T r3 = T(lld * rate * pentry[ke]);
1128 out.space.push_back(row);
1129 out.rate.push_back(r3);
1130 // A branch that cannot happen carries probability zero, not
1131 // one: a PH whose entry vector does not reach every phase
1132 // would otherwise contribute phantom departures.
1133 out.prob.push_back(num_traits<T>::to_double(r3) == 0 ? zero : one);
1134 tag_last(out, hc, 0); // the head of the buffer takes the freed server
1135 }
1136 }
1137 } else if (sched == SchedStrategy::POLLING) {
1138 // A completion ends the VISIT unless the discipline still allows
1139 // another job of the same class; when it ends, the server walks the
1140 // cyclic order to the next tangible controller state.
1141 const PollingInfo<T> pinfo = polling_info(sn, ind);
1142 const T rate = T(mu[k] * phi[k] * kir);
1143 if (num_traits<T>::to_double(rate) <= 0) continue;
1144 std::size_t pos = 0, swk = 0;
1145 long ctr = 0;
1146 polling_get(pinfo, var, cls, pos, swk, ctr);
1147 // No job can complete while the server is walking.
1148 if (swk != 0) continue;
1149 std::vector<long> nbuf(R, 0);
1150 for (std::size_t r = 0; r < R && r < L.bufw; ++r)
1151 nbuf[r] = static_cast<long>(num_traits<T>::to_double(buf[r]));
1152 srv[col] -= one;
1153 long ctrnext = 0;
1154 bool goon = false;
1155 switch (pinfo.ptype) {
1157 ctrnext = 0;
1158 goon = nbuf[cls - 1] > 0; // the visit ends when it drains
1159 break;
1161 ctrnext = ctr - 1; // one of the gated jobs completed
1162 goon = ctrnext > 0;
1163 break;
1165 ctrnext = ctr - 1; // one of the K permitted services used
1166 goon = ctrnext > 0 && nbuf[cls - 1] > 0;
1167 break;
1169 ctrnext = ctr; // the target level is fixed for the visit
1170 goon = nbuf[cls - 1] > ctr;
1171 break;
1172 }
1173 std::size_t q = cls;
1174 int mode = 1;
1175 long budget = ctrnext;
1176 if (!goon) polling_next(pinfo, cls, nbuf, R, false, q, mode, budget);
1177 std::vector<std::vector<T>> rows;
1178 std::vector<T> probs;
1179 polling_land(sn, ist, pinfo, q, mode, budget, buf, srv, var, L, rows, probs);
1180 for (std::size_t j = 0; j < rows.size(); ++j) {
1181 out.space.push_back(rows[j]);
1182 out.rate.push_back(T(lld * rate * probs[j]));
1183 out.prob.push_back(one);
1184 // mode 1 opens a visit, pulling a waiting class-q job into the
1185 // server; a switchover or a park starts nobody
1186 tag_last(out, mode == 1 ? q : 0, 0);
1187 }
1188 } else if (state_detail::buffer_is_tag_phase_pairs(sched)) {
1189 // THE PREEMPT-RESUME / PREEMPT-INDEPENDENT FAMILY, all eight members.
1190 // Every arm of the reference carries the same rate law, mu*phi*kir
1191 // summed over destination phases (afterEventStation.m:1045, :1081,
1192 // :1118, :1150, :1188, :1234, :1286, :1332); what separates them is
1193 // WHICH waiting job is promoted and in WHICH phase it restarts.
1194 const T rate = T(mu[k] * phi[k] * kir);
1195 srv[col] -= one;
1196 const bool prio_aware = sched == SchedStrategy::FCFSPRPRIO ||
1197 sched == SchedStrategy::FCFSPIPRIO ||
1198 sched == SchedStrategy::LCFSPRPRIO ||
1199 sched == SchedStrategy::LCFSPIPRIO;
1200 const bool lcfs = sched == SchedStrategy::LCFSPR ||
1201 sched == SchedStrategy::LCFSPI ||
1202 sched == SchedStrategy::LCFSPRPRIO ||
1203 sched == SchedStrategy::LCFSPIPRIO;
1204 // PR resumes the promoted job in the phase it was interrupted in;
1205 // PI discards that phase and restarts from the entry distribution.
1206 // That single value is the whole PR-vs-PI difference.
1207 const bool resume = sched == SchedStrategy::LCFSPR ||
1208 sched == SchedStrategy::LCFSPRPRIO ||
1209 sched == SchedStrategy::FCFSPR ||
1210 sched == SchedStrategy::FCFSPRPRIO;
1211 // Class column, 0-based and hence EVEN; L.bufw means "nobody waits".
1212 std::size_t pos = L.bufw;
1213 if (ni > S && !suppress_promote && L.bufw >= 2) {
1214 double best = std::numeric_limits<double>::infinity();
1215 if (prio_aware)
1216 // Only the class columns carry a class tag: reading the
1217 // phase columns too would let a phase index masquerade as a
1218 // class and win the group, which is what the reference's
1219 // FCFSPIPRIO/LCFSPIPRIO arms do (:1290, :1336) while its
1220 // PR-PRIO arms correctly restrict to `class_cols` (:1194).
1221 for (std::size_t b = 0; b + 1 < L.bufw; b += 2) {
1222 const double v = num_traits<T>::to_double(buf[b]);
1223 if (v <= 0) continue;
1224 const double p = sn.classes[static_cast<std::size_t>(v) - 1].prio;
1225 if (p < best) best = p;
1226 }
1227 for (std::size_t b = 0; b + 1 < L.bufw; b += 2) {
1228 const double v = num_traits<T>::to_double(buf[b]);
1229 if (v <= 0) continue;
1230 if (prio_aware && sn.classes[static_cast<std::size_t>(v) - 1].prio != best)
1231 continue;
1232 pos = b;
1233 // The buffer is newest-first, so LCFS takes the FIRST such
1234 // pair (:1052 colfirstnnz) and FCFS the LAST (:1121
1235 // colLastNnz). LCFS does NOT take the rightmost slot.
1236 if (lcfs) break;
1237 }
1238 }
1239 if (pos == L.bufw) {
1240 std::vector<T> row = buf;
1241 row.insert(row.end(), srv.begin(), srv.end());
1242 row.insert(row.end(), var.begin(), var.end());
1243 out.space.push_back(row);
1244 out.rate.push_back(T(lld * rate));
1245 out.prob.push_back(one);
1246 continue;
1247 }
1248 const std::size_t hc = static_cast<std::size_t>(num_traits<T>::to_double(buf[pos]));
1249 const std::size_t kst =
1250 static_cast<std::size_t>(num_traits<T>::to_double(buf[pos + 1]));
1251 if (hc == 0 || hc > R || kst == 0 || kst > L.K[hc - 1])
1252 throw InputError("after_event_station_dep: station '" +
1253 sn.stations[ist - 1].name +
1254 "' holds a waiting job whose [class, phase] pair is malformed");
1255 // Close the hole by padding a whole EMPTY PAIR on the left, keeping
1256 // the buffer right-aligned as `from_marginal` enumerates it and as
1257 // afterEventStation.m:1267-1274 requires. Removing the pair in place
1258 // (:1055, :1125) or padding one slot on each side (:1222) leaves a
1259 // layout the enumerator never emits, so the successor is unreachable
1260 // and the generator becomes reducible.
1261 std::vector<T> b2(L.bufw, zero);
1262 for (std::size_t b = 2; b <= pos + 1; ++b) b2[b] = buf[b - 2];
1263 for (std::size_t b = pos + 2; b < L.bufw; ++b) b2[b] = buf[b];
1264 if (resume) {
1265 std::vector<T> s3 = srv;
1266 s3[L.Ks[hc - 1] + kst - 1] += one;
1267 std::vector<T> row = b2;
1268 row.insert(row.end(), s3.begin(), s3.end());
1269 row.insert(row.end(), var.begin(), var.end());
1270 out.space.push_back(row);
1271 out.rate.push_back(T(lld * rate));
1272 out.prob.push_back(one);
1273 tag_last(out, hc, 0); // the promoted job resumes on the freed server
1274 } else {
1275 const std::vector<T> pentry = entry_phase_dist(sn, ist, hc);
1276 for (std::size_t ke = 0; ke < L.K[hc - 1]; ++ke) {
1277 std::vector<T> s3 = srv;
1278 s3[L.Ks[hc - 1] + ke] += one;
1279 std::vector<T> row = b2;
1280 row.insert(row.end(), s3.begin(), s3.end());
1281 row.insert(row.end(), var.begin(), var.end());
1282 out.space.push_back(row);
1283 out.rate.push_back(T(lld * rate * pentry[ke]));
1284 out.prob.push_back(one);
1285 tag_last(out, hc, 0);
1286 }
1287 }
1288 } else if (state_detail::buffer_is_per_class_count(sched)) {
1289 // A per-class-count buffer promotes by COUNT, since no order is
1290 // recorded. SIRO picks the next job at random, so every waiting
1291 // class is a distinct successor with its share of the queue.
1292 srv[col] -= one;
1293 const T rate = T(mu[k] * phi[k] * kir);
1294 double waiting = 0;
1295 for (std::size_t r = 0; r < R; ++r) waiting += num_traits<T>::to_double(buf[r]);
1296 if (waiting <= 0 || suppress_promote) {
1297 std::vector<T> row = buf;
1298 row.insert(row.end(), srv.begin(), srv.end());
1299 row.insert(row.end(), var.begin(), var.end());
1300 out.space.push_back(row);
1301 out.rate.push_back(T(lld * rate));
1302 out.prob.push_back(one);
1303 continue;
1304 }
1305 for (std::size_t r = 1; r <= R; ++r) {
1306 const double nb = num_traits<T>::to_double(buf[r - 1]);
1307 if (nb <= 0) continue;
1308 const std::vector<T> pentry = entry_phase_dist(sn, ist, r);
1309 for (std::size_t ke = 0; ke < L.K[r - 1]; ++ke) {
1310 std::vector<T> b2 = buf, s2 = srv;
1311 b2[r - 1] -= one;
1312 s2[L.Ks[r - 1] + ke] += one;
1313 std::vector<T> row = b2;
1314 row.insert(row.end(), s2.begin(), s2.end());
1315 row.insert(row.end(), var.begin(), var.end());
1316 out.space.push_back(row);
1317 out.rate.push_back(T(lld * rate));
1318 out.prob.push_back(T(num_traits<T>::from_double(nb / waiting) * pentry[ke]));
1319 tag_last(out, r, 0); // the drawn waiting class takes the server
1320 }
1321 }
1322 } else {
1323 throw UnsupportedError(
1324 std::string("after_event_station_dep: the ") + lang::sched_to_text(sched) +
1325 " discipline is not ported yet");
1326 }
1327 }
1328 pad_tags(out);
1329 return out;
1330}
1331
1332/**
1333 * The fraction of the station's capacity a class-`cls` job in phase `k`
1334 * receives, which is the only part of the rate the discipline decides.
1335 *
1336 * PHASE and DEP share this factor exactly: an internal phase transition is
1337 * driven by the same server share as a completion, which is why a job under PS
1338 * advances through its phases more slowly when the station is busy. Only the
1339 * MATRIX differs -- D0(k,kdest) for a phase advance, D1 for a completion.
1340 */
1341template <class T>
1342T service_share(const NetworkStruct<T>& sn, std::size_t ist, const Marginal<T>& m,
1343 std::size_t cls, double ni, double S) {
1344 const std::size_t R = sn.nclasses;
1345 const SchedStrategy sched = sn.stations[ist - 1].sched;
1346 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1347
1348 // The *PRIO variants behave as their base discipline while every job fits
1349 // in a server -- with n <= c nobody is waiting, so precedence is moot. Once
1350 // the station saturates, only the MOST URGENT group present is served, and
1351 // the sharing is computed among that group alone. `prio_pop` is the single
1352 // definition of that group, shared with the load-dependent lookup.
1353 const PrioPop<T> p = prio_pop(sn, ist, m, cls, ni, S);
1354 if (!p.served) return zero; // a lower-priority class gets no service at all
1355 const std::vector<T>& nir = p.nir;
1356 const double nieff = p.ni;
1357
1358 if (sched == SchedStrategy::PS || sched == SchedStrategy::LPS ||
1359 sched == SchedStrategy::PSPRIO)
1360 return nieff > 0 ? num_traits<T>::from_double(std::min(nieff, S) / nieff) : zero;
1361 if (sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
1362 sched == SchedStrategy::DPSPRIO || sched == SchedStrategy::GPSPRIO) {
1363 if (S > 1)
1364 throw UnsupportedError(
1365 "state_events: multi-server DPS/GPS stations are not supported");
1366 const bool dps = sched == SchedStrategy::DPS || sched == SchedStrategy::DPSPRIO;
1367 const std::vector<T>& w = sn.stations[ist - 1].schedparam;
1368 T wsum = zero;
1369 for (std::size_t r = 0; r < R; ++r) wsum += w[r];
1370 T denom = zero;
1371 for (std::size_t r = 0; r < R; ++r) {
1372 // DPS weights by the job COUNTS; GPS shares between the classes
1373 // PRESENT, so each contributes at most one however many it holds.
1374 const T nr = dps ? nir[r] : (num_traits<T>::to_double(nir[r]) > 0 ? one : zero);
1375 denom += T(w[r] / wsum * nr);
1376 }
1377 const T nc = nir[cls - 1];
1378 if (num_traits<T>::to_double(denom) == 0 || num_traits<T>::to_double(nc) == 0)
1379 return zero;
1380 const T sh = T((w[cls - 1] / wsum) / denom);
1381 return dps ? sh : T(sh / nc);
1382 }
1383 // INF and the queueing disciplines: a job in service holds a whole server.
1384 return one;
1385}
1386
1387/**
1388 * Port of the PHASE branch of `State.afterEventStation`: service advances a
1389 * phase WITHOUT completing.
1390 *
1391 * The rate is D0(k, kdest), the off-diagonal of the hidden generator, times the
1392 * same server share a completion gets. Keeping PHASE and DEP on one share is
1393 * what makes a phase-type service slow down consistently under contention; a
1394 * phase advance at full speed under PS would shorten the effective service.
1395 */
1396template <class T>
1398 const std::vector<T>& inspace, std::size_t cls) {
1399 const std::size_t R = sn.nclasses;
1400 const std::size_t ist = sn.nodes[ind - 1].station;
1401 const T one = num_traits<T>::from_int(1);
1402 EventOutcome<T> out;
1403 if (ist == 0) throw InputError("after_event_station_phase: node is not a station");
1404 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1405 const double S = sn.stations[ist - 1].nservers;
1406
1407 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1408 for (std::size_t r = 0; r < R; ++r) {
1409 ph[r] = L.K[r];
1410 shift[r] = L.Ks[r];
1411 }
1412 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1413 if (num_traits<T>::to_double(m.nir[cls - 1]) <= 0) return out;
1414
1415 double ni = 0;
1416 for (std::size_t r = 0; r < R; ++r) ni += num_traits<T>::to_double(m.nir[r]);
1417 // Unlike DEP, the PHASE branch takes the UNMASKED population for both
1418 // factors even at a saturated *PRIO station (afterEventStation.m:1688): the
1419 // masking there is a property of the completion rate, not of the lookup, and
1420 // `service_share` already zeroes a non-urgent class's advance.
1421 const T lld = T(lld_factor(sn, ist, ni) * cd_factor(sn, ist, m.nir, cls));
1422 const T share = service_share(sn, ist, m, cls, ni, S);
1423 const lang::Distrib<T>& d = sn.service[ist - 1][cls - 1];
1424 if (d.D0.rows() != L.K[cls - 1]) return out;
1425
1426 for (std::size_t k = 0; k < L.K[cls - 1]; ++k) {
1427 if (num_traits<T>::to_double(inspace[L.bufw + L.Ks[cls - 1] + k]) <= 0) continue;
1428 for (std::size_t kd = 0; kd < L.K[cls - 1]; ++kd) {
1429 if (kd == k) continue; // the diagonal is the exit rate, not a move
1430 std::vector<T> row = inspace;
1431 row[L.bufw + L.Ks[cls - 1] + k] -= one;
1432 row[L.bufw + L.Ks[cls - 1] + kd] += one;
1433 out.space.push_back(row);
1434 out.rate.push_back(T(lld * d.D0(k, kd) * m.kir[cls - 1][k] * share));
1435 out.prob.push_back(one);
1436 }
1437 }
1438 return out;
1439}
1440
1441/**
1442 * Port of the RENEGE branch: a WAITING class-`cls` job abandons the queue.
1443 *
1444 * Patience is exponential, so every waiting job abandons at the same rate and
1445 * the aggregate out of this state is (waiting count) * mu. Which job leaves is
1446 * therefore immaterial -- waiting jobs are exchangeable under memoryless
1447 * patience -- so the reference removes the first tagged slot and re-pads a zero
1448 * on the left, keeping the buffer in the right-aligned form the arrival handler
1449 * expects.
1450 */
1451template <class T>
1453 const std::vector<T>& inspace, std::size_t cls,
1454 const T& impatience_mu) {
1455 const std::size_t R = sn.nclasses;
1456 const std::size_t ist = sn.nodes[ind - 1].station;
1457 EventOutcome<T> out;
1458 if (ist == 0) throw InputError("after_event_station_renege: node is not a station");
1459 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1460
1461 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1462 for (std::size_t r = 0; r < R; ++r) {
1463 ph[r] = L.K[r];
1464 shift[r] = L.Ks[r];
1465 }
1466 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1467 const double waiting = num_traits<T>::to_double(m.nir[cls - 1]) -
1468 num_traits<T>::to_double(m.sir[cls - 1]);
1469 if (waiting <= 0) return out;
1470
1471 const T tag = num_traits<T>::from_int(static_cast<long>(cls));
1472 std::size_t slot = L.bufw;
1473 for (std::size_t b = 0; b < L.bufw; ++b)
1474 if (inspace[b] == tag) { slot = b; break; }
1475 if (slot == L.bufw) return out;
1476
1477 std::vector<T> row = inspace;
1478 for (std::size_t b = slot; b > 0; --b) row[b] = row[b - 1];
1479 row[0] = num_traits<T>::from_int(0);
1480 out.space.push_back(row);
1481 out.rate.push_back(T(num_traits<T>::from_double(waiting) * impatience_mu));
1482 out.prob.push_back(num_traits<T>::from_int(1));
1483 return out;
1484}
1485
1486/**
1487 * Port of the RETRY branch: an ORBITING class-`cls` job retries entry.
1488 *
1489 * The retry succeeds only when a server is free; otherwise the job stays in
1490 * orbit and the event is not generated at all, which is exactly what
1491 * distinguishes a retrial queue from a queue whose buffer is called an orbit.
1492 *
1493 * @param constant_policy CONSTANT retrial: one controller retries for the whole
1494 * orbit, so the rate does NOT scale with the orbit size. Under the
1495 * default LINEAR policy every orbiting job carries its own timer and the
1496 * aggregate rate is (orbit size) * mu.
1497 * @param sn the refreshed network struct
1498 * @param ind index of the station the event fires at
1499 * @param inspace the state the event is applied to
1500 * @param cls class of the retrying job
1501 * @param retrial_mu retrial rate of that class
1502 */
1503template <class T>
1505 const std::vector<T>& inspace, std::size_t cls,
1506 const T& retrial_mu, bool constant_policy = false) {
1507 const std::size_t R = sn.nclasses;
1508 const std::size_t ist = sn.nodes[ind - 1].station;
1509 const T one = num_traits<T>::from_int(1);
1510 EventOutcome<T> out;
1511 if (ist == 0) throw InputError("after_event_station_retry: node is not a station");
1512 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
1513 const double S = sn.stations[ist - 1].nservers;
1514
1515 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1516 for (std::size_t r = 0; r < R; ++r) {
1517 ph[r] = L.K[r];
1518 shift[r] = L.Ks[r];
1519 }
1520 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
1521 const double orbit = num_traits<T>::to_double(m.nir[cls - 1]) -
1522 num_traits<T>::to_double(m.sir[cls - 1]);
1523 double occ = 0;
1524 for (std::size_t j = 0; j < L.srvw; ++j) occ += num_traits<T>::to_double(inspace[L.bufw + j]);
1525 if (orbit <= 0 || occ >= S) return out;
1526
1527 const T tag = num_traits<T>::from_int(static_cast<long>(cls));
1528 std::size_t slot = L.bufw;
1529 for (std::size_t b = 0; b < L.bufw; ++b)
1530 if (inspace[b] == tag) { slot = b; break; }
1531 if (slot == L.bufw) return out;
1532
1533 const std::vector<T> pentry = entry_phase_dist(sn, ist, cls);
1534 const T agg = constant_policy ? retrial_mu
1535 : T(num_traits<T>::from_double(orbit) * retrial_mu);
1536 for (std::size_t ke = 0; ke < L.K[cls - 1]; ++ke) {
1537 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
1538 std::vector<T> row = inspace;
1539 for (std::size_t b = slot; b > 0; --b) row[b] = row[b - 1];
1540 row[0] = num_traits<T>::from_int(0);
1541 row[L.bufw + L.Ks[cls - 1] + ke] += one;
1542 out.space.push_back(row);
1543 out.rate.push_back(T(agg * pentry[ke]));
1544 out.prob.push_back(one);
1545 // A successful retry is the only way into the server at a retrial
1546 // station (its departures never promote from the orbit), so it carries
1547 // the START the startRate == TN + preemptRate identity needs.
1548 tag_last(out, cls, 0);
1549 }
1550 pad_tags(out);
1551 return out;
1552}
1553
1554/**
1555 * Port of the FAILURE and REPAIR branches: the server goes down, or comes back.
1556 *
1557 * Only the STATUS column moves, which is the trailing local variable. Jobs in
1558 * service are NOT lost: service is memoryless here, so an interrupted job
1559 * resumes on repair with no state to remember. The passive half of the
1560 * synchronization is LOCAL, so no job moves anywhere in the network either.
1561 *
1562 * @param up true for REPAIR (0 -> 1), false for FAILURE (1 -> 0)
1563 * @param mu breakdownMu for a failure, repairMu for a repair
1564 * @param sn the refreshed network struct
1565 * @param ind index of the station the event fires at
1566 * @param inspace the state the event is applied to
1567 */
1568template <class T>
1570 const std::vector<T>& inspace, bool up,
1571 const T& mu) {
1572 EventOutcome<T> out;
1573 if (inspace.empty()) return out;
1574 (void)sn;
1575 (void)ind;
1576 const double status = num_traits<T>::to_double(inspace.back());
1577 // A failure needs an UP server and a repair a DOWN one; anything else is
1578 // not an admissible transition and must produce no edge at all.
1579 if ((up && status != 0) || (!up && status != 1)) return out;
1580 std::vector<T> row = inspace;
1581 row.back() = num_traits<T>::from_int(up ? 1 : 0);
1582 out.space.push_back(row);
1583 out.rate.push_back(mu);
1584 out.prob.push_back(num_traits<T>::from_int(1));
1585 return out;
1586}
1587
1588/**
1589 * Port of `State.afterEventFork`: an event at a STATEFUL Fork node.
1590 *
1591 * The fork's state is a plain per-class count of PARENT jobs momentarily held
1592 * between their arrival and the firing. An arrival buffers one; a DEPARTURE DOES
1593 * NOT EXIST, because the multi-branch emission is atomic across several nodes and
1594 * cannot be decomposed into a departure here plus an arrival there --
1595 * `refresh_sync` emits no DEP sync for a Fork and `after_fj_event` fires instead.
1596 *
1597 * The arrival rate is left UNSET (the reference writes -1) because an ARV is the
1598 * PASSIVE half of a synchronization: the rate belongs to the active departure
1599 * upstream.
1600 */
1601template <class T>
1603 const std::vector<T>& inspace, EventType event,
1604 std::size_t cls) {
1605 const std::size_t R = sn.nclasses;
1606 EventOutcome<T> out;
1607 if (event != EventType::ARV) return out;
1608 if (inspace.size() < R)
1609 throw InputError("after_event_fork: the Fork state row at node '" + sn.nodes[ind - 1].name +
1610 "' is narrower than the class set");
1611 std::vector<T> row = inspace;
1612 // The counts are the LAST R columns, which is where `from_marginal_node`
1613 // puts them and where `after_fj_event` reads them.
1614 row[row.size() - R + cls - 1] += num_traits<T>::from_int(1);
1615 out.space.push_back(row);
1616 out.rate.push_back(num_traits<T>::from_int(-1));
1617 out.prob.push_back(num_traits<T>::from_int(1));
1618 return out;
1619}
1620
1621/**
1622 * Port of `State.afterEventJoin`: an event at a Join node of an FJ-augmented
1623 * struct.
1624 *
1625 * The join's state is a plain per-class count vector of BUFFERED jobs, so it
1626 * bypasses the buffer/server/local slicing every other station takes -- a join
1627 * performs no service, it performs a rendezvous.
1628 *
1629 * ARV buffers the arriving job or sibling. DEP in an ORIGINAL class r fires when
1630 * either a plain (never-forked) class-r job is buffered, or some tag has its full
1631 * required sibling multiset present; the firing consumes the siblings of the
1632 * LOWEST complete tag, which is the same canonical choice the fork's allocation
1633 * makes and is what keeps the two in step. DEP in an AUXILIARY class is refused
1634 * outright: a sibling never departs on its own, it is consumed by the parent's
1635 * firing, and letting it depart would release a job the fork never emitted.
1636 */
1637template <class T>
1639 const std::vector<T>& inspace, EventType event,
1640 std::size_t cls) {
1641 const std::size_t R = sn.nclasses;
1642 const T one = num_traits<T>::from_int(1);
1643 EventOutcome<T> out;
1644 if (inspace.size() < R)
1645 throw InputError("after_event_join: the Join state row at node '" + sn.nodes[ind - 1].name +
1646 "' is narrower than the class set");
1647 const std::size_t off = inspace.size() - R;
1648
1649 if (event == EventType::ARV) {
1650 std::vector<T> row = inspace;
1651 row[off + cls - 1] += one;
1652 out.space.push_back(row);
1653 out.rate.push_back(num_traits<T>::from_int(-1));
1654 out.prob.push_back(one);
1655 return out;
1656 }
1657 if (event != EventType::DEP) return out;
1658
1659 const typename std::map<std::size_t, FjJoinParam>::const_iterator jit =
1660 sn.fjjoinparam.find(ind);
1661 const FjJoinParam* fjp = jit == sn.fjjoinparam.end() ? 0 : &jit->second;
1662
1663 if (fjp) {
1664 for (std::size_t x = 0; x < fjp->origclasses.size(); ++x) {
1665 const std::map<std::size_t, std::vector<std::vector<std::size_t>>>::const_iterator ait =
1666 fjp->auxmatrix.find(fjp->origclasses[x]);
1667 if (ait == fjp->auxmatrix.end()) continue;
1668 for (std::size_t b = 0; b < ait->second.size(); ++b)
1669 for (std::size_t t = 0; t < ait->second[b].size(); ++t)
1670 if (ait->second[b][t] == cls) return out; // an auxiliary class
1671 }
1672 }
1673
1675 if (num_traits<T>::to_double(inspace[off + cls - 1]) > 0) {
1676 // A plain job that never went through the fork: it passes straight
1677 // through, since a join is a rendezvous only for siblings.
1678 std::vector<T> row = inspace;
1679 row[off + cls - 1] -= one;
1680 out.space.push_back(row);
1681 out.rate.push_back(imm);
1682 out.prob.push_back(one);
1683 return out;
1684 }
1685 if (!fjp) return out;
1686 const std::map<std::size_t, std::vector<std::vector<std::size_t>>>::const_iterator ait =
1687 fjp->auxmatrix.find(cls);
1688 const std::map<std::size_t, std::vector<std::size_t>>::const_iterator rit =
1689 fjp->required.find(cls);
1690 if (ait == fjp->auxmatrix.end() || rit == fjp->required.end()) return out;
1691 const std::vector<std::vector<std::size_t>>& aux = ait->second;
1692 const std::vector<std::size_t>& req = rit->second;
1693 if (aux.empty()) return out;
1694 const std::size_t B = aux.size(), Tt = aux[0].size();
1695 for (std::size_t t = 0; t < Tt; ++t) {
1696 bool complete = true;
1697 for (std::size_t b = 0; b < B && complete; ++b) {
1698 const std::size_t a = aux[b][t];
1699 const double need = b < req.size() ? static_cast<double>(req[b]) : 1.0;
1700 if (num_traits<T>::to_double(inspace[off + a - 1]) < need) complete = false;
1701 }
1702 if (!complete) continue;
1703 std::vector<T> row = inspace;
1704 for (std::size_t b = 0; b < B; ++b) {
1705 const std::size_t a = aux[b][t];
1706 const long need = b < req.size() ? static_cast<long>(req[b]) : 1;
1707 row[off + a - 1] -= num_traits<T>::from_int(need);
1708 }
1709 out.space.push_back(row);
1710 out.rate.push_back(imm);
1711 out.prob.push_back(one);
1712 return out; // the LOWEST complete tag only; the rest are permutations
1713 }
1714 return out;
1715}
1716
1717/**
1718 * Port of `State.afterEventStation`'s dispatch: the successors of one event at
1719 * one station.
1720 *
1721 * The event-specific rates that are not derivable from `sn` alone -- patience,
1722 * retrial and breakdown -- are passed in, because the reference reads them from
1723 * fields (`impatienceMu`, `retrialMu`, `breakdownMu`) that this port has not
1724 * yet grown. Every rate that IS derivable is computed from the struct.
1725 */
1726template <class T>
1727void rr_advance_row(const NetworkStruct<T>& sn, std::size_t ind, std::size_t cls,
1728 std::vector<std::vector<T>>& rows);
1729
1730/**
1731 * Port of `State.afterEventRouter`: a Router holds a job for the instant it
1732 * takes to decide where it goes.
1733 *
1734 * The row is [per-class counts | local vars], with no buffer and no phase: a
1735 * Router serves nothing, so there is no service state to carry. An ARRIVAL adds
1736 * the job at an UNSPECIFIED rate (-1), which is the reference's marker for a
1737 * passive half whose rate the active half sets; a DEPARTURE removes it at the
1738 * Immediate rate and advances the dispatch pointer, so the router never holds a
1739 * job for a positive length of time.
1740 */
1741template <class T>
1743 const std::vector<T>& inspace, EventType event,
1744 std::size_t cls) {
1745 EventOutcome<T> out;
1746 const std::size_t R = sn.nclasses;
1747 if (inspace.size() < R) return out;
1748 const T one = num_traits<T>::from_int(1);
1749 if (event == EventType::ARV) {
1750 std::vector<T> row = inspace;
1751 row[cls - 1] = T(row[cls - 1] + one);
1752 out.space.push_back(row);
1753 // Passive action: the rate is the active half's, not this node's.
1754 out.rate.push_back(num_traits<T>::from_int(-1));
1755 out.prob.push_back(one);
1756 return out;
1757 }
1758 if (event == EventType::DEP) {
1759 if (!(num_traits<T>::to_double(inspace[cls - 1]) > 0)) return out;
1760 std::vector<T> row = inspace;
1761 row[cls - 1] = T(row[cls - 1] - one);
1762 out.space.push_back(row);
1764 out.prob.push_back(one);
1765 rr_advance_row(sn, ind, cls, out.space);
1766 return out;
1767 }
1768 return out;
1769}
1770
1771/**
1772 * Advance the round-robin dispatch pointer of (IND, CLS) in every successor row.
1773 *
1774 * The local-variable block is the TAIL of a state row, so the pointer is located
1775 * from the right; `rr_var_slot` gives its 1-based index inside that block. A
1776 * no-op wherever the pair does not dispatch round-robin.
1777 */
1778template <class T>
1779void rr_advance_row(const NetworkStruct<T>& sn, std::size_t ind, std::size_t cls,
1780 std::vector<std::vector<T>>& rows) {
1781 if (sn.rr_var_slot(ind, cls) == 0) return;
1782 const std::size_t w = sn.nvars_of(ind);
1783 if (w == 0) return;
1784 for (std::size_t i = 0; i < rows.size(); ++i) {
1785 if (rows[i].size() < w) continue;
1786 std::vector<T> var(rows[i].end() - w, rows[i].end());
1787 sn.rr_advance(ind, cls, var);
1788 std::copy(var.begin(), var.end(), rows[i].end() - w);
1789 }
1790}
1791
1792template <class T>
1794 const std::vector<T>& inspace, EventType event,
1795 std::size_t cls, bool no_promote = false,
1796 const T& aux_rate = num_traits<T>::from_int(0)) {
1797 switch (event) {
1798 case EventType::ARV:
1799 // A signal class arriving is not an arrival at all: it removes
1800 // jobs and is annihilated, so it never reaches the scheduling
1801 // branches. A REPLY signal is the exception -- it completes a
1802 // synchronous call and then joins as an ordinary job.
1803 if (sn.issignal.size() >= cls && sn.issignal[cls - 1]) {
1804 if (!(sn.signaltype.size() >= cls &&
1805 sn.signaltype[cls - 1] == lang::SignalType::REPLY))
1806 return after_event_station_signal(sn, ind, inspace, cls);
1807 // A REPLY takes the reply path only at a station that actually
1808 // holds a block for it; elsewhere it is a plain job class and
1809 // falls through to ordinary arrival handling.
1810 if (reply_block_info(sn, ind).width > 0)
1811 return after_event_station_reply(sn, ind, inspace, cls);
1812 }
1813 return after_event_station_arv(sn, ind, inspace, cls);
1814 case EventType::DEP: {
1815 EventOutcome<T> out = after_event_station_dep(sn, ind, inspace, cls, no_promote);
1816 // ROUND-ROBIN DISPATCH advances on every completion, which is what
1817 // makes the next destination deterministic; the generator then reads
1818 // the pointer OUT OF THIS SUCCESSOR to pick the link. Without the
1819 // advance the pointer is a frozen coordinate and every job takes the
1820 // same link, which is random routing with the wrong support rather
1821 // than round robin. Reference: `afterEventStation.m:632-658`.
1822 rr_advance_row(sn, ind, cls, out.space);
1823 // TRUE BAS, the departure half. When the marker is already set the
1824 // front job has COMPLETED and is being held, so this DEP is not a
1825 // service completion at all -- it is the instant transfer of that
1826 // held job downstream, which the generator only offers when the
1827 // destination has room. It therefore fires at the Immediate rate and
1828 // CLEARS the marker; the complementary become-blocked edge (0 -> 1) is
1829 // added by the generator, the only place that can see the
1830 // destination's occupancy.
1831 //
1832 // Gate on `isbasblocking`, not on this station's own drop rule: under
1833 // the destination declaration form the rule is not here.
1834 if (ind <= sn.isbasblocking.size() && sn.isbasblocking[ind - 1] &&
1835 !inspace.empty() && num_traits<T>::to_double(inspace.back()) == 1 &&
1836 !out.space.empty()) {
1837 // 1e7 VERBATIM, not GlobalConstants::Immediate (1e8). The rate is
1838 // large but FINITE, so the blocked states keep a proportional
1839 // share of the stationary mass and the analyzer's queue-length
1840 // shift reads it; using 1e8 would divide that share by ten and
1841 // move every reported queue length. The reference hardcodes 1e7.
1842 const T imm = num_traits<T>::from_double(1e7);
1843 for (std::size_t i = 0; i < out.space.size(); ++i) {
1844 out.space[i].back() = num_traits<T>::from_int(0);
1845 out.rate[i] = imm;
1846 }
1847 }
1848 return out;
1849 }
1850 case EventType::PHASE:
1851 return after_event_station_phase(sn, ind, inspace, cls);
1852 case EventType::RENEGE:
1853 return after_event_station_renege(sn, ind, inspace, cls, aux_rate);
1854 case EventType::RETRY:
1855 return after_event_station_retry(sn, ind, inspace, cls, aux_rate);
1856 case EventType::SWITCH:
1857 return after_event_station_switch(sn, ind, inspace, cls);
1858 case EventType::FAILURE:
1859 return after_event_station_breakdown(sn, ind, inspace, false, aux_rate);
1860 case EventType::REPAIR:
1861 return after_event_station_breakdown(sn, ind, inspace, true, aux_rate);
1862 case EventType::LOCAL:
1863 return EventOutcome<T>(); // a dummy event moves nothing
1864 default:
1865 throw UnsupportedError(std::string("after_event_station: the ") +
1866 lang::event_to_text(event) +
1867 " event is not ported yet");
1868 }
1869}
1870
1871/**
1872 * Port of `State.afterEventTransition`, the PHASE arm: one running server of
1873 * the given mode advances its firing phase (`cls` is interpreted as the MODE,
1874 * as in the reference). ENABLE and FIRE are global events handled by
1875 * `after_global_event`, so they return an empty outcome here, matching the
1876 * reference's no-op arms.
1877 *
1878 * RATE. The MATLAB body multiplies the phase-k move rate by BOTH
1879 * kir(:,mode,k) and nir(mode) (afterEventTransition.m:38-40), but nir is the
1880 * sum of kir over the phases, so the extra factor counts the running servers
1881 * twice; the JAR and the native python carry D0(k,kdest) * kir alone, and
1882 * this port follows them.
1883 *
1884 * The row layout is the one `after_global_event` slices:
1885 * [buf(nmodes) | srv(sum fK) | fired(nmodes) | var].
1886 */
1887template <class T>
1889 const std::vector<T>& inspace, EventType event,
1890 std::size_t mode) {
1891 EventOutcome<T> out;
1892 if (event != EventType::PHASE) return out; // ENABLE / FIRE are global
1893 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
1894 sn.transparam.find(ind);
1895 if (it == sn.transparam.end())
1896 throw InputError("after_event_transition: node has no TransitionParam");
1897 const TransitionParam<T>& tp = it->second;
1898 if (mode == 0 || mode > tp.nmodes)
1899 throw InputError("after_event_transition: mode index is out of range");
1900 const T one = num_traits<T>::from_int(1);
1901
1902 std::vector<std::size_t> fK(tp.nmodes, 1), fKs(tp.nmodes, 0);
1903 std::size_t tot = 0;
1904 for (std::size_t m = 0; m < tp.nmodes; ++m) {
1905 fK[m] = m < tp.firingphases.size() && tp.firingphases[m] > 0 ? tp.firingphases[m] : 1;
1906 fKs[m] = tot;
1907 tot += fK[m];
1908 }
1909 if (fK[mode - 1] <= 1) return out; // a single phase has no internal move
1910 if (mode - 1 >= tp.firingproc.size() ||
1911 tp.firingproc[mode - 1].D0.rows() != fK[mode - 1])
1912 return out;
1913 const Matrix<T>& D0 = tp.firingproc[mode - 1].D0;
1914
1915 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
1916 const std::size_t idx = tp.nmodes + fKs[mode - 1] + k;
1917 const T cnt = inspace[idx];
1918 if (!(num_traits<T>::to_double(cnt) > 0)) continue;
1919 for (std::size_t kd = 0; kd < fK[mode - 1]; ++kd) {
1920 if (kd == k) continue; // the diagonal is the exit rate, not a move
1921 if (!(num_traits<T>::to_double(D0(k, kd)) > 0)) continue;
1922 std::vector<T> row = inspace;
1923 row[idx] -= one;
1924 row[tp.nmodes + fKs[mode - 1] + kd] += one;
1925 out.space.push_back(row);
1926 out.rate.push_back(T(D0(k, kd) * cnt));
1927 out.prob.push_back(one);
1928 }
1929 }
1930 return out;
1931}
1932
1933/**
1934 * Port of `State.afterEvent`: the successors of one event at one NODE.
1935 *
1936 * The reference's body is mostly slicing -- it cuts `inspace` into buffer,
1937 * server and local-variable blocks and hands the pieces to the per-node-type
1938 * handler. This port slices inside each handler instead (`row_layout`), so what
1939 * remains here is the dispatch itself and the guards that precede it.
1940 *
1941 * A class the station does not accept short-circuits: `phases_of` is zero
1942 * there, and every downstream index into the server block would be out of
1943 * range. That guard is the reference's `K(class) == 0` test.
1944 *
1945 * `cls` IS A MODE, NOT A CLASS, on a Transition's PHASE action; see the guard.
1946 */
1947template <class T>
1949 const std::vector<T>& inspace, EventType event, std::size_t cls,
1950 bool no_promote = false,
1951 const T& aux_rate = num_traits<T>::from_int(0)) {
1952 if (ind == 0 || ind > sn.nodes.size())
1953 throw InputError("after_event: node index is out of range");
1954 const NodeDef& nd = sn.nodes[ind - 1];
1955 // THE `cls` SLOT IS NOT ALWAYS A CLASS. On a Transition's PHASE action it
1956 // carries the MODE, which `refresh_sync` puts there deliberately ("one
1957 // server phase-change action per MODE, not per class") and
1958 // `after_event_transition` reads back as such. Bounding it by `nclasses`
1959 // refused every mode past the class count: spn_basic_closed is one class
1960 // and three modes, so modes 2 and 3 raised "class index is out of range"
1961 // and no closed SPN with more modes than classes could be walked at all.
1962 // The mode bound belongs to the handler, which already applies it against
1963 // `tp.nmodes`, so only the non-Transition case is checked here.
1964 const bool cls_is_mode = (nd.nodetype == NodeType::Transition && event == EventType::PHASE);
1965 if (!cls_is_mode && (cls == 0 || cls > sn.nclasses))
1966 throw InputError("after_event: class index is out of range");
1967
1968 // A Join of an FJ-augmented struct IS a station, but its state is a bare
1969 // per-class count vector: it performs a rendezvous, not a service, so it must
1970 // bypass the buffer/server slicing before `after_event_station` sees it.
1971 if (sn.isfjaugmented && nd.nodetype == NodeType::Join)
1972 return after_event_join(sn, ind, inspace, event, cls);
1973
1974 if (nd.station != 0) {
1975 // A class with no service process at this station cannot be involved
1976 // in any event here.
1977 if (sn.phases_of(nd.station, cls) == 0) return EventOutcome<T>();
1978 return after_event_station(sn, ind, inspace, event, cls, no_promote, aux_rate);
1979 }
1980 if (!nd.stateful) return EventOutcome<T>(); // a stateless node holds nothing
1981
1982 if (nd.nodetype == NodeType::Cache) return after_event_cache(sn, ind, inspace, event, cls);
1983 if (nd.nodetype == NodeType::Fork) return after_event_fork(sn, ind, inspace, event, cls);
1984 if (nd.nodetype == NodeType::Transition)
1985 return after_event_transition(sn, ind, inspace, event, cls);
1986 if (nd.nodetype == NodeType::Router) return after_event_router(sn, ind, inspace, event, cls);
1987
1988 throw UnsupportedError("after_event: events at stateful non-station node '" + nd.name +
1989 "' are not ported yet");
1990}
1991
1992namespace signal_detail {
1993
1994/** Merge duplicate destinations so the generator sees one entry per state. */
1995template <class T>
1996void merge_states(std::vector<std::vector<T>>& sp, std::vector<T>& pr) {
1997 std::vector<std::vector<T>> us;
1998 std::vector<T> up;
1999 for (std::size_t i = 0; i < sp.size(); ++i) {
2000 std::size_t at = us.size();
2001 for (std::size_t j = 0; j < us.size(); ++j)
2002 if (us[j] == sp[i]) { at = j; break; }
2003 if (at == us.size()) {
2004 us.push_back(sp[i]);
2005 up.push_back(pr[i]);
2006 } else {
2007 up[at] += pr[i];
2008 }
2009 }
2010 sp.swap(us);
2011 pr.swap(up);
2012}
2013
2014} // namespace signal_detail
2015
2016/**
2017 * Port of `State.signalBatchPMF`: the batch size a negative signal removes.
2018 *
2019 * The pmf is CLIPPED at the eligible population: an oversized batch empties
2020 * the station rather than driving the queue negative, so the whole tail
2021 * P(B >= n) lumps onto "remove all n". That is the same clipping LDES applies
2022 * with min(B,n) and the tail term MAM uses.
2023 */
2024template <class T>
2025std::pair<std::vector<std::size_t>, std::vector<T>> signal_batch_pmf(
2026 const NetworkStruct<T>& sn, std::size_t cls, std::size_t ntot) {
2027 std::vector<std::size_t> kv;
2028 std::vector<T> kp;
2029 if (sn.signalremdist.size() < cls || sn.signalremdist[cls - 1].empty()) {
2030 kv.push_back(1);
2031 kp.push_back(num_traits<T>::from_int(1));
2032 return std::make_pair(kv, kp);
2033 }
2034 const std::vector<T>& d = sn.signalremdist[cls - 1];
2035 T head_sum = num_traits<T>::from_int(0);
2036 for (std::size_t b = 0; b < ntot; ++b) {
2037 const T p = b < d.size() ? d[b] : num_traits<T>::from_int(0);
2038 if (num_traits<T>::to_double(p) > 0) {
2039 kv.push_back(b);
2040 kp.push_back(p);
2041 }
2042 head_sum += p;
2043 }
2044 const double tail = 1.0 - num_traits<T>::to_double(head_sum);
2045 if (tail > 0) {
2046 kv.push_back(ntot);
2047 kp.push_back(num_traits<T>::from_double(tail));
2048 }
2049 if (kv.empty()) {
2050 kv.push_back(1);
2051 kp.push_back(num_traits<T>::from_int(1));
2052 return std::make_pair(kv, kp);
2053 }
2054 T tot = num_traits<T>::from_int(0);
2055 for (std::size_t i = 0; i < kp.size(); ++i) tot += kp[i];
2056 if (num_traits<T>::to_double(tot) > 0)
2057 for (std::size_t i = 0; i < kp.size(); ++i) kp[i] = T(kp[i] / tot);
2058 return std::make_pair(kv, kp);
2059}
2060
2061/**
2062 * Port of `State.afterEventStationSignal`: a G-network signal arrives.
2063 *
2064 * A signal NEVER joins the station. It removes jobs already there and is
2065 * annihilated, so the event is passive throughout and the successors differ
2066 * only in which victims were taken.
2067 *
2068 * Victim selection has two tiers, and conflating them is the trap: FCFS and
2069 * LCFS rank by AGE, which only an ordered buffer records, so at a per-class
2070 * count buffer an age policy degenerates to a uniform draw. They also drain
2071 * the waiting line completely before touching a server, whereas RANDOM draws
2072 * uniformly across waiting and in-service jobs alike.
2073 */
2074template <class T>
2076 const std::vector<T>& inspace, std::size_t cls) {
2077 const std::size_t R = sn.nclasses;
2078 const std::size_t ist = sn.nodes[ind - 1].station;
2079 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
2080 const T minus_one = num_traits<T>::from_int(-1);
2081 EventOutcome<T> out;
2082 if (ist == 0) throw InputError("after_event_station_signal: node is not a station");
2083 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2084 const SchedStrategy sched = sn.stations[ist - 1].sched;
2085 const double S = sn.stations[ist - 1].nservers;
2086
2087 std::vector<std::size_t> ph(R, 1), shift(R, 0);
2088 for (std::size_t r = 0; r < R; ++r) {
2089 ph[r] = L.K[r];
2090 shift[r] = L.Ks[r];
2091 }
2092 const Marginal<T> m = to_marginal(sn, ist, inspace, ph, shift, L.nvar);
2093
2094 // A CATASTROPHE empties the station outright, ignoring the batch pmf: a
2095 // catastrophe removes every job by definition.
2096 if (sn.signaltype.size() >= cls && sn.signaltype[cls - 1] == lang::SignalType::CATASTROPHE) {
2097 std::vector<T> row(L.bufw + L.srvw, zero);
2098 row.insert(row.end(), inspace.end() - L.nvar, inspace.end());
2099 out.space.push_back(row);
2100 out.rate.push_back(minus_one);
2101 out.prob.push_back(one);
2102 return out;
2103 }
2104
2105 // Eligible victim classes: the declared target, or every non-signal class
2106 // for the classic untargeted negative customer.
2107 std::vector<std::size_t> tgt;
2108 const std::size_t declared = sn.signaltarget.size() >= cls ? sn.signaltarget[cls - 1] : 0;
2109 if (declared >= 1) {
2110 tgt.push_back(declared);
2111 } else {
2112 for (std::size_t r = 1; r <= R; ++r)
2113 if (sn.issignal.size() < r || !sn.issignal[r - 1]) tgt.push_back(r);
2114 }
2115 std::vector<std::size_t> elig;
2116 std::size_t ntot = 0;
2117 for (std::size_t i = 0; i < tgt.size(); ++i) {
2118 const double nr = num_traits<T>::to_double(m.nir[tgt[i] - 1]);
2119 if (nr > 0) {
2120 elig.push_back(tgt[i]);
2121 ntot += static_cast<std::size_t>(nr);
2122 }
2123 }
2124 if (elig.empty()) {
2125 // No victim: the signal simply vanishes, leaving the state unchanged.
2126 out.space.push_back(inspace);
2127 out.rate.push_back(minus_one);
2128 out.prob.push_back(one);
2129 return out;
2130 }
2131
2132 const lang::RemovalPolicy policy = sn.signalrempolicy.size() >= cls
2133 ? sn.signalrempolicy[cls - 1]
2135 const bool ordered = state_detail::buffer_is_class_tag(sched) ||
2136 state_detail::buffer_is_tag_phase_pairs(sched);
2137 const bool paired = state_detail::buffer_is_tag_phase_pairs(sched);
2138 const bool counted = state_detail::buffer_is_per_class_count(sched);
2139 const std::pair<std::vector<std::size_t>, std::vector<T>> pmf =
2140 signal_batch_pmf(sn, cls, ntot);
2141
2142 std::vector<std::vector<T>> acc;
2143 std::vector<T> accp;
2144 for (std::size_t ik = 0; ik < pmf.first.size(); ++ik) {
2145 if (num_traits<T>::to_double(pmf.second[ik]) <= 0) continue;
2146 std::vector<std::vector<T>> cur(1, inspace);
2147 std::vector<T> curp(1, one);
2148 // Remove one job at a time: sequential draws without replacement give
2149 // a uniform choice of the removed SUBSET.
2150 for (std::size_t step = 0; step < pmf.first[ik]; ++step) {
2151 std::vector<std::vector<T>> nxt;
2152 std::vector<T> nxtp;
2153 for (std::size_t row = 0; row < cur.size(); ++row) {
2154 std::vector<T> buf(cur[row].begin(), cur[row].begin() + L.bufw);
2155 std::vector<T> srv(cur[row].begin() + L.bufw,
2156 cur[row].begin() + L.bufw + L.srvw);
2157 const std::vector<T> var(cur[row].begin() + L.bufw + L.srvw, cur[row].end());
2158
2159 // Waiting victims, as (position, class, multiplicity).
2160 std::vector<std::size_t> wpos, wcls, wwt;
2161 if (ordered) {
2162 for (std::size_t b = 0; b < L.bufw; b += paired ? 2 : 1) {
2163 const double v = num_traits<T>::to_double(buf[b]);
2164 if (v <= 0) continue;
2165 bool ok = false;
2166 for (std::size_t e = 0; e < elig.size(); ++e)
2167 if (elig[e] == static_cast<std::size_t>(v)) ok = true;
2168 if (!ok) continue;
2169 wpos.push_back(b);
2170 wcls.push_back(static_cast<std::size_t>(v));
2171 wwt.push_back(1);
2172 }
2173 } else if (counted) {
2174 for (std::size_t e = 0; e < elig.size(); ++e) {
2175 const std::size_t r = elig[e];
2176 if (r > L.bufw) continue;
2177 const double v = num_traits<T>::to_double(buf[r - 1]);
2178 if (v <= 0) continue;
2179 wpos.push_back(r - 1);
2180 wcls.push_back(r);
2181 wwt.push_back(static_cast<std::size_t>(v));
2182 }
2183 }
2184 // In-service victims, per (class, phase).
2185 std::vector<std::size_t> scls, sph, scnt;
2186 for (std::size_t e = 0; e < elig.size(); ++e) {
2187 const std::size_t r = elig[e];
2188 for (std::size_t p = 0; p < L.K[r - 1]; ++p) {
2189 const double v = num_traits<T>::to_double(srv[L.Ks[r - 1] + p]);
2190 if (v <= 0) continue;
2191 scls.push_back(r);
2192 sph.push_back(p);
2193 scnt.push_back(static_cast<std::size_t>(v));
2194 }
2195 }
2196 std::size_t nwait = 0, nsrv = 0;
2197 for (std::size_t i = 0; i < wwt.size(); ++i) nwait += wwt[i];
2198 for (std::size_t i = 0; i < scnt.size(); ++i) nsrv += scnt[i];
2199 if (nwait == 0 && nsrv == 0) {
2200 // Already drained: nothing left for this step to remove.
2201 nxt.push_back(cur[row]);
2202 nxtp.push_back(curp[row]);
2203 continue;
2204 }
2205
2206 std::vector<std::vector<T>> sp;
2207 std::vector<T> pr;
2208 const bool age_ordered =
2209 ordered && (policy == lang::RemovalPolicy::FCFS ||
2210 policy == lang::RemovalPolicy::LCFS);
2211 if (age_ordered && nwait > 0) {
2212 // The head of line is the LAST occupied slot, the newest
2213 // arrival the first: the buffer is right-aligned.
2214 std::size_t pick = 0;
2215 for (std::size_t i = 0; i < wpos.size(); ++i)
2216 if (policy == lang::RemovalPolicy::FCFS ? wpos[i] > wpos[pick]
2217 : wpos[i] < wpos[pick])
2218 pick = i;
2219 std::vector<T> b2 = buf;
2220 b2.erase(b2.begin() + wpos[pick], b2.begin() + wpos[pick] + (paired ? 2 : 1));
2221 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2222 std::vector<T> nr = b2;
2223 nr.insert(nr.end(), srv.begin(), srv.end());
2224 nr.insert(nr.end(), var.begin(), var.end());
2225 sp.push_back(nr);
2226 pr.push_back(one);
2227 } else {
2228 // RANDOM draws over everything present; FCFS/LCFS at a
2229 // count buffer drain the waiting line first, and only reach
2230 // the servers once it is empty.
2231 const std::size_t total = policy == lang::RemovalPolicy::RANDOM
2232 ? nwait + nsrv
2233 : (nwait > 0 ? nwait : nsrv);
2234 for (std::size_t i = 0; i < wpos.size(); ++i) {
2235 std::vector<T> b2 = buf;
2236 if (counted) {
2237 b2[wcls[i] - 1] -= one;
2238 } else {
2239 b2.erase(b2.begin() + wpos[i],
2240 b2.begin() + wpos[i] + (paired ? 2 : 1));
2241 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2242 }
2243 std::vector<T> nr = b2;
2244 nr.insert(nr.end(), srv.begin(), srv.end());
2245 nr.insert(nr.end(), var.begin(), var.end());
2246 sp.push_back(nr);
2247 pr.push_back(num_traits<T>::from_double(static_cast<double>(wwt[i]) /
2248 static_cast<double>(total)));
2249 }
2250 if (policy == lang::RemovalPolicy::RANDOM || nwait == 0) {
2251 for (std::size_t i = 0; i < scls.size(); ++i) {
2252 std::vector<T> b2 = buf, s2 = srv;
2253 s2[L.Ks[scls[i] - 1] + sph[i]] -= one;
2254 // The freed server pulls in the head of line, where
2255 // the station keeps one at all.
2256 double occ = 0;
2257 for (std::size_t j = 0; j < s2.size(); ++j)
2258 occ += num_traits<T>::to_double(s2[j]);
2259 if (L.bufw > 0 && occ < S) {
2260 if (ordered) {
2261 std::size_t hp = L.bufw;
2262 for (std::size_t b = 0; b < L.bufw; b += paired ? 2 : 1)
2263 if (num_traits<T>::to_double(b2[b]) > 0) hp = b;
2264 if (hp != L.bufw) {
2265 const std::size_t pc = static_cast<std::size_t>(
2266 num_traits<T>::to_double(b2[hp]));
2267 std::size_t pp = 0;
2268 if (paired) {
2269 const double v =
2270 num_traits<T>::to_double(b2[hp + 1]);
2271 pp = v >= 1 ? static_cast<std::size_t>(v) - 1 : 0;
2272 }
2273 b2.erase(b2.begin() + hp,
2274 b2.begin() + hp + (paired ? 2 : 1));
2275 b2.insert(b2.begin(), paired ? 2 : 1, zero);
2276 s2[L.Ks[pc - 1] + pp] += one;
2277 }
2278 } else if (counted) {
2279 // A count buffer carries no order, so the
2280 // lowest-indexed waiting class is promoted
2281 // to keep the map single-valued; the actual
2282 // service order is resolved by the rates.
2283 for (std::size_t r = 1; r <= R && r <= L.bufw; ++r)
2284 if (num_traits<T>::to_double(b2[r - 1]) > 0) {
2285 b2[r - 1] -= one;
2286 s2[L.Ks[r - 1]] += one;
2287 break;
2288 }
2289 }
2290 }
2291 std::vector<T> nr = b2;
2292 nr.insert(nr.end(), s2.begin(), s2.end());
2293 nr.insert(nr.end(), var.begin(), var.end());
2294 sp.push_back(nr);
2295 pr.push_back(num_traits<T>::from_double(
2296 static_cast<double>(scnt[i]) / static_cast<double>(total)));
2297 }
2298 }
2299 }
2300 signal_detail::merge_states(sp, pr);
2301 for (std::size_t i = 0; i < sp.size(); ++i) {
2302 nxt.push_back(sp[i]);
2303 nxtp.push_back(T(curp[row] * pr[i]));
2304 }
2305 }
2306 signal_detail::merge_states(nxt, nxtp);
2307 cur.swap(nxt);
2308 curp.swap(nxtp);
2309 }
2310 for (std::size_t i = 0; i < cur.size(); ++i) {
2311 acc.push_back(cur[i]);
2312 accp.push_back(T(pmf.second[ik] * curp[i]));
2313 }
2314 }
2315 signal_detail::merge_states(acc, accp);
2316 out.space.swap(acc);
2317 out.prob.swap(accp);
2318 out.rate.assign(out.space.size(), minus_one);
2319 return out;
2320}
2321
2322/**
2323 * Port of `State.passAndSwap`: the transition a service completion triggers at
2324 * a pass-and-swap station (Dorsman and Gardner 2024, Sect. 2.3).
2325 *
2326 * The completing job scans FORWARD from its own position for the first job it
2327 * may swap with per the graph G, takes that job's place and ejects it; the
2328 * ejected job repeats the scan. The chain ends at a job with no swappable
2329 * successor, and THAT job departs -- which is why the departing class is in
2330 * general not the class whose service completed.
2331 *
2332 * @param c 0-based list of 1-based class indices, oldest first
2333 * @param p 0-based position whose service token completed
2334 * @param G class-compatibility graph, G[a][b] true when a may swap with b
2335 * @return (the list after the transition, the 1-based departing class)
2336 */
2337template <class T>
2338std::pair<std::vector<std::size_t>, std::size_t> pass_and_swap(
2339 const std::vector<std::size_t>& c, std::size_t p,
2340 const std::vector<std::vector<bool>>& G) {
2341 const std::size_t n = c.size();
2342 if (p >= n) throw InputError("pass_and_swap: position is out of range for the state");
2343 std::vector<std::size_t> chain(1, p);
2344 std::size_t moving = c[p], cur = p;
2345 for (;;) {
2346 std::size_t q = n;
2347 for (std::size_t j = cur + 1; j < n; ++j)
2348 if (moving - 1 < G.size() && c[j] - 1 < G[moving - 1].size() &&
2349 G[moving - 1][c[j] - 1]) {
2350 q = j;
2351 break;
2352 }
2353 if (q == n) break; // no swappable successor: this job departs
2354 chain.push_back(q);
2355 moving = c[q];
2356 cur = q;
2357 }
2358 const std::size_t dep = c[chain.back()];
2359 // Shift classes one step along the chain; the last is overwritten because
2360 // it departed, and the head-of-chain slot is then removed.
2361 std::vector<std::size_t> cnew = c;
2362 for (std::size_t i = 0; i + 1 < chain.size(); ++i) cnew[chain[i + 1]] = c[chain[i]];
2363 cnew.erase(cnew.begin() + chain[0]);
2364 return std::make_pair(cnew, dep);
2365}
2366
2367/**
2368 * Port of `State.afterEventStationPAS`: events at a pass-and-swap station.
2369 *
2370 * The state here is NOT the [buffer | server] split every other discipline
2371 * uses: it is the full ordered list of class indices, left-aligned and zero
2372 * padded, with no server block at all. Service is governed by the rate
2373 * function mu(c) rather than by a per-class rate, so the whole notion of "in
2374 * service" is replaced by a token at each position.
2375 */
2376/**
2377 * Per-position service rate increments Delta_mu(c1..cp) = mu(c1..cp) - mu(c1..c_{p-1}).
2378 */
2379template <class T, class F>
2380inline std::vector<double> pas_increments(const F& mu_fun, const std::vector<std::size_t>& c) {
2381 std::vector<double> inc(c.size(), 0.0);
2382 double mu_prev = 0.0;
2383 for (std::size_t p = 0; p < c.size(); ++p) {
2384 const std::vector<std::size_t> prefix(c.begin(), c.begin() + p + 1);
2385 const double mu_cur = num_traits<T>::to_double(mu_fun(prefix));
2386 inc[p] = mu_cur - mu_prev;
2387 mu_prev = mu_cur;
2388 }
2389 return inc;
2390}
2391
2392/**
2393 * Tag the successor just appended to OUT with the PAS positions that started
2394 * service on it: those of CNEW that are served (Delta_mu > 0) and were not
2395 * served in COLD. Under a swap the tag follows the POSITION rather than the job
2396 * identity, since pass-and-swap redefines which job holds a position.
2397 */
2398template <class T, class F>
2399inline void pas_tag_started(EventOutcome<T>& out, const F& mu_fun,
2400 const std::vector<std::size_t>& cold,
2401 const std::vector<std::size_t>& cnew) {
2402 if (!mu_fun || out.space.empty()) return;
2403 const std::vector<double> inc_new = pas_increments<T>(mu_fun, cnew);
2404 const std::vector<double> inc_old = pas_increments<T>(mu_fun, cold);
2405 for (std::size_t p = 0; p < inc_new.size(); ++p) {
2406 if (inc_new[p] <= 0) continue;
2407 if (p < inc_old.size() && inc_old[p] > 0) continue; // already served
2408 tag_last(out, cnew[p], 0);
2409 }
2410}
2411
2412template <class T>
2414 const std::vector<T>& inspace, EventType event,
2415 std::size_t cls) {
2416 const std::size_t ist = sn.nodes[ind - 1].station;
2417 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2418 EventOutcome<T> out;
2419 if (ist == 0) throw InputError("after_event_station_pas: node is not a station");
2420 const typename std::map<std::size_t, typename NetworkStruct<T>::PasParam>::const_iterator it =
2421 sn.pasparam.find(ist);
2422 if (it == sn.pasparam.end() || !it->second.svc_rate_fun)
2423 throw InputError(
2424 "after_event_station_pas: the station has no service rate function mu(c); set one "
2425 "with set_pas");
2426
2427 const std::size_t V = sn.nvars_of(ind);
2428 const std::size_t W = inspace.size() - V;
2429 std::vector<std::size_t> c;
2430 for (std::size_t i = 0; i < W; ++i) {
2431 const double v = num_traits<T>::to_double(inspace[i]);
2432 if (v > 0) c.push_back(static_cast<std::size_t>(v));
2433 }
2434 const std::vector<T> var(inspace.begin() + W, inspace.end());
2435 const double cap = sn.cap[ist - 1];
2436
2437 if (event == EventType::ARV) {
2438 // The arrival joins at the BACK of the list, which is what records the
2439 // order the rate function is a function of.
2440 if (static_cast<double>(c.size()) >= cap) return out; // full: lost
2441 std::vector<std::size_t> nc = c;
2442 nc.push_back(cls);
2443 // NO SLOT IN THE ENCODING IS A BLOCK, NOT A LOSS. The list occupies one
2444 // row position per job, so a row of width W holds W jobs; emitting a
2445 // successor here and letting `row.resize(W, zero)` cut the list back to
2446 // W would DESTROY the job that did not fit while reporting a state that
2447 // looks exactly like the pre-arrival one -- a customer silently gone
2448 // from a closed network, with no error anywhere. Returning no successor
2449 // disables the upstream departure instead, which is what every other
2450 // discipline's capacity filter does and what the enumerated CTMC space
2451 // does at its own width boundary.
2452 if (nc.size() > W) return out;
2453 std::vector<T> row;
2454 for (std::size_t i = 0; i < nc.size(); ++i)
2455 row.push_back(num_traits<T>::from_int(static_cast<long>(nc[i])));
2456 row.resize(W, zero);
2457 row.insert(row.end(), var.begin(), var.end());
2458 out.space.push_back(row);
2459 out.rate.push_back(num_traits<T>::from_int(-1));
2460 out.prob.push_back(one);
2461 // A PAS station has one clock for the whole station and no server to
2462 // hold, so "in service" means "at a position whose rate increment
2463 // Delta_mu is positive": a job starts exactly when a position goes from
2464 // a zero increment to a positive one. With mu(c) = 1 only the head is
2465 // served (M/M/1), with mu(c) = |c| every position is (M/M/inf), and the
2466 // rule reproduces both.
2467 pas_tag_started<T>(out, it->second.svc_rate_fun, c, nc);
2468 pad_tags(out);
2469 return out;
2470 }
2471 if (event != EventType::DEP) return out; // PAS service is exponential: no PHASE
2472
2473 // Each position holds a service token firing at the INCREMENT of mu over
2474 // the prefix ending there. Summing the increments telescopes to mu(c), so
2475 // the station's total service rate is exactly the rate function.
2476 T mu_prev = zero;
2477 for (std::size_t p = 0; p < c.size(); ++p) {
2478 const std::vector<std::size_t> prefix(c.begin(), c.begin() + p + 1);
2479 const T mu_cur = it->second.svc_rate_fun(prefix);
2480 const T ratep = T(mu_cur - mu_prev);
2481 mu_prev = mu_cur;
2482 if (num_traits<T>::to_double(ratep) <= 0) continue; // position unserved
2483 const std::pair<std::vector<std::size_t>, std::size_t> ps =
2484 pass_and_swap<T>(c, p, it->second.swap_graph);
2485 // The completing token need not eject its own class, so only the
2486 // positions whose chain ends in THIS class contribute to its departure.
2487 if (ps.second != cls) continue;
2488 std::vector<T> row;
2489 for (std::size_t i = 0; i < ps.first.size(); ++i)
2490 row.push_back(num_traits<T>::from_int(static_cast<long>(ps.first[i])));
2491 row.resize(W, zero);
2492 row.insert(row.end(), var.begin(), var.end());
2493 out.space.push_back(row);
2494 out.rate.push_back(ratep);
2495 out.prob.push_back(one);
2496 pas_tag_started<T>(out, it->second.svc_rate_fun, c, ps.first);
2497 }
2498 pad_tags(out);
2499 return out;
2500}
2501
2502/**
2503 * Port of `State.replyBlockInfo`: the layout of the reply block.
2504 *
2505 * The block trails the modulation, routing and shared-node columns of nvars,
2506 * occupying columns 2R+1+r. Appending is deliberate -- every existing nvars
2507 * reader keeps its indices, and the columns stay zero-width for models without
2508 * reply signals, so no other model changes state width.
2509 */
2510template <class T>
2512 const std::size_t R = sn.nclasses;
2513 ReplyBlockInfo ri;
2514 ri.slot.assign(R, static_cast<std::size_t>(-1));
2515 if (sn.nvars.size() < ind || sn.nvars[ind - 1].size() < 3 * R + 1) return ri;
2516 std::size_t pos = 0;
2517 for (std::size_t j = 0; j < 2 * R + 1; ++j) pos += sn.nvars[ind - 1][j];
2518 for (std::size_t r = 1; r <= R; ++r)
2519 if (sn.nvars[ind - 1][2 * R + r] > 0) {
2520 ri.slot[r - 1] = pos;
2521 ri.classes.push_back(r);
2522 ++pos;
2523 ++ri.width;
2524 }
2525 return ri;
2526}
2527
2528/** How many servers node `ind` is holding for pending replies, from its vars. */
2529template <class T>
2530double reply_blocked(const NetworkStruct<T>& sn, std::size_t ind, const std::vector<T>& var) {
2531 const ReplyBlockInfo ri = reply_block_info(sn, ind);
2532 double nb = 0;
2533 for (std::size_t i = 0; i < ri.classes.size(); ++i) {
2534 const std::size_t s = ri.slot[ri.classes[i] - 1];
2535 // The slot is an index into the local-variable block, which is the
2536 // TAIL of the row, so it is offset from the start of `var`.
2537 if (s != static_cast<std::size_t>(-1) && s < var.size())
2538 nb += num_traits<T>::to_double(var[s]);
2539 }
2540 return nb;
2541}
2542
2543/**
2544 * Port of `State.afterEventStationReply`: a REPLY signal completes a
2545 * synchronous call at the station holding the server for it.
2546 *
2547 * A REPLY is not a negative customer. It releases one held server and then
2548 * JOINS as an ordinary job carrying the call result onward, so unlike
2549 * NEGATIVE or CATASTROPHE it is not annihilated.
2550 *
2551 * PASS-THROUGH is the subtle part: the released server is taken by the reply
2552 * ITSELF, never by a waiting job. The reply is work this station already paid
2553 * for, so queueing it behind the residents both misreports its residence and
2554 * steals capacity. Its service is typically Immediate, so the server is handed
2555 * straight back and the ordinary departure path then promotes the head of
2556 * line -- which also keeps the occupancy within the server count, unlike
2557 * admitting the reply on top of a promoted job.
2558 */
2559template <class T>
2561 const std::vector<T>& inspace, std::size_t cls) {
2562 const std::size_t R = sn.nclasses;
2563 const std::size_t ist = sn.nodes[ind - 1].station;
2564 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2565 EventOutcome<T> out;
2566 if (ist == 0) throw InputError("after_event_station_reply: node is not a station");
2567 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2568 const ReplyBlockInfo ri = reply_block_info(sn, ind);
2569 const double S = sn.stations[ist - 1].nservers;
2570
2571 // The calling class this reply releases: the one whose expected reply IS
2572 // this class and which holds a block here.
2573 std::size_t callclass = 0;
2574 for (std::size_t i = 0; i < ri.classes.size(); ++i) {
2575 const std::size_t r = ri.classes[i];
2576 if (sn.syncreply.size() >= r && sn.syncreply[r - 1] == cls) {
2577 callclass = r;
2578 break;
2579 }
2580 }
2581
2582 std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
2583 std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
2584 std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
2585
2586 if (callclass > 0) {
2587 const std::size_t s = ri.slot[callclass - 1];
2588 if (s != static_cast<std::size_t>(-1) && s < var.size() &&
2589 num_traits<T>::to_double(var[s]) > 0)
2590 var[s] -= one;
2591 }
2592
2593 // The reply joins: into a free server, enumerating its entry phase, or at
2594 // the tail of the buffer. Servers still held for OTHER pending replies are
2595 // not available, which is what `Seff` subtracts.
2596 const double seff = S - reply_blocked(sn, ind, var);
2597 double occ = 0;
2598 for (std::size_t j = 0; j < srv.size(); ++j) occ += num_traits<T>::to_double(srv[j]);
2599 if (occ < seff) {
2600 const std::vector<T> pentry = entry_phase_dist(sn, ist, cls);
2601 for (std::size_t ke = 0; ke < L.K[cls - 1]; ++ke) {
2602 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
2603 std::vector<T> s2 = srv;
2604 s2[L.Ks[cls - 1] + ke] += one;
2605 std::vector<T> row = buf;
2606 row.insert(row.end(), s2.begin(), s2.end());
2607 row.insert(row.end(), var.begin(), var.end());
2608 out.space.push_back(row);
2609 out.rate.push_back(num_traits<T>::from_int(-1));
2610 out.prob.push_back(pentry[ke]);
2611 }
2612 return out;
2613 }
2614 // Every available server is busy: queue at the tail, which for a
2615 // right-aligned buffer is the LAST empty slot.
2616 std::vector<T> b2 = buf;
2617 std::size_t slot = b2.size();
2618 for (std::size_t b = 0; b < b2.size(); ++b)
2619 if (num_traits<T>::to_double(b2[b]) == 0) slot = b;
2620 if (slot == b2.size()) {
2621 b2.insert(b2.begin(), zero);
2622 slot = 0;
2623 }
2624 b2[slot] = num_traits<T>::from_int(static_cast<long>(cls));
2625 std::vector<T> row = b2;
2626 row.insert(row.end(), srv.begin(), srv.end());
2627 row.insert(row.end(), var.begin(), var.end());
2628 out.space.push_back(row);
2629 out.rate.push_back(num_traits<T>::from_int(-1));
2630 out.prob.push_back(one);
2631 return out;
2632}
2633
2634
2635/** Read (pos, swk, ctr) out of the local-variable block. */
2636template <class T>
2637void polling_get(const PollingInfo<T>& pi, const std::vector<T>& var, std::size_t srvclass,
2638 std::size_t& pos, std::size_t& swk, long& ctr) {
2639 pos = pi.ipos != static_cast<std::size_t>(-1)
2640 ? static_cast<std::size_t>(num_traits<T>::to_double(var[pi.off + pi.ipos]))
2641 : (srvclass > 0 ? srvclass : 1);
2642 swk = pi.iswk != static_cast<std::size_t>(-1)
2643 ? static_cast<std::size_t>(num_traits<T>::to_double(var[pi.off + pi.iswk]))
2644 : 0;
2645 ctr = pi.ictr != static_cast<std::size_t>(-1)
2646 ? static_cast<long>(num_traits<T>::to_double(var[pi.off + pi.ictr]))
2647 : 0;
2648}
2649
2650/** Write (pos, swk, ctr) back into the local-variable block. */
2651template <class T>
2652std::vector<T> polling_set(const PollingInfo<T>& pi, std::vector<T> var, std::size_t pos,
2653 std::size_t swk, long ctr) {
2654 if (pi.ipos != static_cast<std::size_t>(-1))
2655 var[pi.off + pi.ipos] = num_traits<T>::from_int(static_cast<long>(pos));
2656 if (pi.iswk != static_cast<std::size_t>(-1))
2657 var[pi.off + pi.iswk] = num_traits<T>::from_int(static_cast<long>(swk));
2658 if (pi.ictr != static_cast<std::size_t>(-1))
2659 var[pi.off + pi.ictr] = num_traits<T>::from_int(ctr);
2660 return var;
2661}
2662
2663/** Port of `State.pollingBudget`: how many services this visit may perform. */
2664template <class T>
2665long polling_budget(const PollingInfo<T>& pi, long nbufq) {
2666 switch (pi.ptype) {
2667 case lang::PollingType::EXHAUSTIVE: return 0; // unused: drains instead
2668 case lang::PollingType::GATED: return nbufq; // exactly those found
2669 case lang::PollingType::KLIMITED: return static_cast<long>(pi.pk);
2670 case lang::PollingType::DECREMENTING: return nbufq - 1;
2671 default: throw InputError("polling_budget: unsupported polling type");
2672 }
2673}
2674
2675/**
2676 * Port of `State.pollingNext`: where the server goes from buffer `pos`.
2677 *
2678 * Returns mode 1 to open a visit at q, 2 to enter the switchover into q, and 0
2679 * to PARK. Parking is reachable only with every switchover immediate, where a
2680 * server completing a full lap without finding work would otherwise cycle in
2681 * zero time forever.
2682 */
2683template <class T>
2684void polling_next(const PollingInfo<T>& pi, std::size_t pos, const std::vector<long>& nbuf,
2685 std::size_t R, bool arrived, std::size_t& q, int& mode, long& budget) {
2686 if (arrived && pi.polled[pos - 1] && nbuf[pos - 1] > 0) {
2687 // The switchover into pos is already paid for, so a visit starts here.
2688 q = pos;
2689 mode = 1;
2690 budget = polling_budget(pi, nbuf[pos - 1]);
2691 return;
2692 }
2693 std::size_t p = pos;
2694 for (std::size_t step = 0; step < R; ++step) { // a full lap, ending at pos
2695 p = p % R + 1;
2696 if (!pi.polled[p - 1]) continue;
2697 if (pi.has_sw[p - 1]) {
2698 q = p;
2699 mode = 2;
2700 budget = 0;
2701 return;
2702 }
2703 if (nbuf[p - 1] > 0) {
2704 q = p;
2705 mode = 1;
2706 budget = polling_budget(pi, nbuf[p - 1]);
2707 return;
2708 }
2709 }
2710 q = pos;
2711 mode = 0;
2712 budget = 0;
2713}
2714
2715/** Port of `State.pollingLand`: the states the walk lands in, with weights. */
2716template <class T>
2717void polling_land(const NetworkStruct<T>& sn, std::size_t ist, const PollingInfo<T>& pi,
2718 std::size_t q, int mode, long budget, const std::vector<T>& buf,
2719 const std::vector<T>& srv, const std::vector<T>& var, const RowLayout<T>& L,
2720 std::vector<std::vector<T>>& rows, std::vector<T>& probs) {
2721 const T one = num_traits<T>::from_int(1);
2722 if (mode == 1) {
2723 // Open or continue a visit at q: pull a waiting class-q job in.
2724 std::vector<T> b2 = buf;
2725 b2[q - 1] -= one;
2726 const std::vector<T> pentry = entry_phase_dist(sn, ist, q);
2727 for (std::size_t ke = 0; ke < L.K[q - 1]; ++ke) {
2728 if (num_traits<T>::to_double(pentry[ke]) <= 0) continue;
2729 std::vector<T> s2 = srv;
2730 s2[L.Ks[q - 1] + ke] += one;
2731 std::vector<T> row = b2;
2732 row.insert(row.end(), s2.begin(), s2.end());
2733 const std::vector<T> v2 = polling_set(pi, var, q, 0, budget);
2734 row.insert(row.end(), v2.begin(), v2.end());
2735 rows.push_back(row);
2736 probs.push_back(pentry[ke]);
2737 }
2738 } else if (mode == 2) {
2739 // Enter the switchover into q: the facility stays EMPTY while walking.
2740 for (std::size_t ke = 0; ke < pi.ksw[q - 1]; ++ke) {
2741 if (num_traits<T>::to_double(pi.sw_pie[q - 1][ke]) <= 0) continue;
2742 std::vector<T> row = buf;
2743 row.insert(row.end(), srv.begin(), srv.end());
2744 const std::vector<T> v2 = polling_set(pi, var, q, ke + 1, 0);
2745 row.insert(row.end(), v2.begin(), v2.end());
2746 rows.push_back(row);
2747 probs.push_back(pi.sw_pie[q - 1][ke]);
2748 }
2749 } else {
2750 // Park: held until the next arrival wakes the server.
2751 std::vector<T> row = buf;
2752 row.insert(row.end(), srv.begin(), srv.end());
2753 const std::vector<T> v2 = polling_set(pi, var, q, 0, 0);
2754 row.insert(row.end(), v2.begin(), v2.end());
2755 rows.push_back(row);
2756 probs.push_back(one);
2757 }
2758}
2759
2760
2761/**
2762 * Port of the SWITCH branch: a polling server advances its switchover timer.
2763 *
2764 * Unlike PHASE, which carries only the internal transitions of a phase-type and
2765 * leaves the absorption to DEP, this event carries BOTH -- a completed
2766 * switchover moves no job, so there is no departure to attach the absorption
2767 * to. It is therefore emitted even for a single-phase switchover, where it
2768 * consists of the absorption alone.
2769 */
2770template <class T>
2772 const std::vector<T>& inspace, std::size_t cls) {
2773 const std::size_t R = sn.nclasses;
2774 const std::size_t ist = sn.nodes[ind - 1].station;
2775 const T one = num_traits<T>::from_int(1);
2776 EventOutcome<T> out;
2777 if (ist == 0) throw InputError("after_event_station_switch: node is not a station");
2778 const PollingInfo<T> pinfo = polling_info(sn, ind);
2779 if (!pinfo.valid || !pinfo.has_sw[cls - 1]) return out;
2780 const RowLayout<T> L = row_layout(sn, ind, inspace.size());
2781
2782 const std::vector<T> buf(inspace.begin(), inspace.begin() + L.bufw);
2783 const std::vector<T> srv(inspace.begin() + L.bufw, inspace.begin() + L.bufw + L.srvw);
2784 const std::vector<T> var(inspace.begin() + L.bufw + L.srvw, inspace.end());
2785
2786 std::size_t pos = 0, swk = 0;
2787 long ctr = 0;
2788 polling_get(pinfo, var, 0, pos, swk, ctr);
2789 // The server must actually be inside the switchover into this buffer.
2790 if (pos != cls || swk == 0) return out;
2791
2792 // Internal transitions of the switchover phase-type.
2793 for (std::size_t kd = 0; kd < pinfo.ksw[cls - 1]; ++kd) {
2794 if (kd + 1 == swk) continue;
2795 const T r0 = pinfo.sw_d0[cls - 1](swk - 1, kd);
2796 if (num_traits<T>::to_double(r0) <= 0) continue;
2797 std::vector<T> row = buf;
2798 row.insert(row.end(), srv.begin(), srv.end());
2799 const std::vector<T> v2 = polling_set(pinfo, var, cls, kd + 1, 0);
2800 row.insert(row.end(), v2.begin(), v2.end());
2801 out.space.push_back(row);
2802 out.rate.push_back(r0);
2803 out.prob.push_back(one);
2804 }
2805 // Absorption: the server arrives and either opens a visit or walks on.
2806 T rate = num_traits<T>::from_int(0);
2807 for (std::size_t j = 0; j < pinfo.ksw[cls - 1]; ++j) rate += pinfo.sw_d1[cls - 1](swk - 1, j);
2808 if (num_traits<T>::to_double(rate) <= 0) return out;
2809 std::vector<long> nbuf(R, 0);
2810 for (std::size_t r = 0; r < R && r < L.bufw; ++r)
2811 nbuf[r] = static_cast<long>(num_traits<T>::to_double(buf[r]));
2812 std::size_t q = 0;
2813 int mode = 0;
2814 long budget = 0;
2815 polling_next(pinfo, cls, nbuf, R, true, q, mode, budget);
2816 std::vector<std::vector<T>> rows;
2817 std::vector<T> probs;
2818 polling_land(sn, ist, pinfo, q, mode, budget, buf, srv, var, L, rows, probs);
2819 for (std::size_t j = 0; j < rows.size(); ++j) {
2820 // A switchover completing over an empty buffer starts the next leg at
2821 // once, and when that leg re-enters the SAME phase of the same
2822 // switchover the landing state IS the departure state. Such a self-loop
2823 // is not a transition: emitting it would inflate the row's exit rate.
2824 if (rows[j] == inspace) continue;
2825 out.space.push_back(rows[j]);
2826 out.rate.push_back(T(rate * probs[j]));
2827 out.prob.push_back(one);
2828 // A completed switchover that opens a visit pulls a waiting class-q job
2829 // into the server, so it starts service just as an ARV or a DEP
2830 // promotion does. This is the one service start a polling station
2831 // reaches through neither, and leaving it untagged would break
2832 // startRate == TN + preemptRate there for no reason other than the name
2833 // of the carrier event.
2834 tag_last(out, mode == 1 ? q : 0, 0);
2835 }
2836 pad_tags(out);
2837 return out;
2838}
2839
2840
2841/**
2842 * Port of `State.afterEventCache`: events at a Cache node.
2843 *
2844 * A Cache is stateful but is NOT a station, so its row is [per-class counts |
2845 * cache contents | retrieval bitmap] with no buffer or server block. The
2846 * contents region holds one column per cached slot, laid out list by list;
2847 * `cpos(i,j)` is position j of list i.
2848 *
2849 * A READ is INSTANTANEOUS: every branch fires at `GlobalConstants::Immediate`,
2850 * because the read is a routing decision rather than a service. The job enters
2851 * in its read class and leaves in the hit or miss class, so the transition
2852 * both moves the job between classes and rewrites the cache contents.
2853 */
2854template <class T>
2856 const std::vector<T>& inspace, EventType event,
2857 std::size_t cls) {
2858 const std::size_t R = sn.nclasses;
2859 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
2861 EventOutcome<T> out;
2862 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci = sn.nodeparam.find(ind);
2863 if (ci == sn.nodeparam.end()) throw InputError("after_event_cache: node has no CacheParam");
2864 const CacheParam<T>& cp = ci->second;
2865 const std::size_t h = cp.itemcap.size();
2866 const std::size_t n = cp.nitems;
2867 std::size_t tcc = 0; // total cache capacity, the width of the contents region
2868 for (std::size_t i = 0; i < h; ++i)
2869 if (cp.itemcap[i] > 0) tcc += static_cast<std::size_t>(cp.itemcap[i]);
2870 // cpos(i,j): position j (1-based) of list i (1-based) in the contents region.
2871 const std::vector<int>& m = cp.itemcap;
2872 struct Cpos {
2873 const std::vector<int>& m;
2874 std::size_t operator()(std::size_t i, std::size_t j) const {
2875 std::size_t base = 0;
2876 for (std::size_t t = 0; t + 1 < i; ++t) base += static_cast<std::size_t>(m[t]);
2877 return base + j - 1;
2878 }
2879 } cpos{m};
2880
2881 std::vector<T> srv(inspace.begin(), inspace.begin() + R);
2882 std::vector<T> var(inspace.begin() + R, inspace.end());
2883
2884 if (event == EventType::ARV) {
2885 srv[cls - 1] += one;
2886 std::vector<T> row = srv;
2887 row.insert(row.end(), var.begin(), var.end());
2888 out.space.push_back(row);
2889 out.rate.push_back(num_traits<T>::from_int(-1)); // passive
2890 out.prob.push_back(one);
2891 return out;
2892 }
2893
2894 if (event == EventType::DEP) {
2895 if (num_traits<T>::to_double(srv[cls - 1]) <= 0) return out;
2896 // A departure only moves the job: the occupancy bit of a fetch was
2897 // already set by the READ that began it, exactly as the reference's DEP
2898 // branch does nothing beyond decrementing the class.
2899 srv[cls - 1] -= one;
2900 std::vector<T> row = srv;
2901 row.insert(row.end(), var.begin(), var.end());
2902 out.space.push_back(row);
2903 out.rate.push_back(imm); // the departure is instantaneous
2904 out.prob.push_back(one);
2905 return out;
2906 }
2907
2908 if (event != EventType::READ) return out;
2909
2910 // A READ needs exactly one job present, and it must be of the reading class.
2911 double tot = 0;
2912 for (std::size_t r = 0; r < R; ++r) tot += num_traits<T>::to_double(srv[r]);
2913 if (num_traits<T>::to_double(srv[cls - 1]) <= 0 || tot != 1) return out;
2914 if (cls - 1 >= cp.pread.size() || cp.pread[cls - 1].empty()) return out;
2915 const std::vector<T>& p = cp.pread[cls - 1];
2916
2917 // The delayed-hit retrieval system. Block A (n columns) marks the items in
2918 // flight; block B (one column per retrieval class) counts the secondary
2919 // requests merged onto those fetches. Its width is read off the row rather
2920 // than off the parameters, because a struct whose space predates the block
2921 // still has to be walkable.
2922 const bool retr = cp.retrieval_capacity > 0 && !cp.retrieval_classes.empty();
2923 std::vector<std::size_t> rc_list, rc_items, rc_orig;
2924 if (retr) cache_retrieval_class_map(cp, rc_list, rc_items, rc_orig);
2925 const std::size_t block_b = tcc + n;
2926 std::size_t width_b = var.size() > block_b ? var.size() - block_b : 0;
2927 if (width_b != rc_list.size()) width_b = 0;
2928 // -1 is the sample path's unbounded merge; an exact solver enumerates block
2929 // B only up to the level it declared, and a merge past that level is refused
2930 // rather than folded into a state the space does not hold.
2931 const double maxpend = cp.max_pending_retrieval < 0
2932 ? std::numeric_limits<double>::infinity()
2933 : static_cast<double>(cp.max_pending_retrieval);
2934 bool from_retrieval = false;
2935 for (std::size_t j = 0; j < rc_list.size(); ++j)
2936 if (rc_list[j] == cls) from_retrieval = true;
2937
2938 for (std::size_t k = 1; k <= n; ++k) {
2939 if (k - 1 >= p.size() || num_traits<T>::to_double(p[k - 1]) <= 0) continue;
2940 std::vector<T> srv_e = srv;
2941 srv_e[cls - 1] -= one;
2942 // The item is searched ONLY in the contents region; the trailing
2943 // retrieval slots are a different namespace.
2944 std::size_t posk = 0;
2945 for (std::size_t c = 0; c < tcc && c < var.size(); ++c)
2946 if (static_cast<std::size_t>(num_traits<T>::to_double(var[c])) == k) {
2947 posk = c + 1;
2948 break;
2949 }
2950 // A RETURNING RETRIEVAL always completes the miss that started it, so it
2951 // takes the miss branch even where the enumeration produced a (then
2952 // unreachable) state holding item k. A retrieval class has no hit class
2953 // to switch into either.
2954 if (from_retrieval) posk = 0;
2955 const Matrix<T>& ac = cp.accost.empty() || cls - 1 >= cp.accost.size() ||
2956 k - 1 >= cp.accost[cls - 1].size()
2957 ? Matrix<T>()
2958 : cp.accost[cls - 1][k - 1];
2959 const bool have_ac = ac.rows() >= h + 1 && ac.cols() >= h + 1;
2960
2961 if (posk == 0) {
2962 // CACHE MISS, or one leg of a retrieval. A fetch is in flight iff
2963 // block A's bit for item k is set.
2964 const bool in_flight =
2965 retr && tcc + k <= var.size() && num_traits<T>::to_double(var[tcc + k - 1]) != 0;
2966 std::size_t r_class = 0;
2967 if (retr && k - 1 < cp.retrieval_classes.size() &&
2968 cls - 1 < cp.retrieval_classes[k - 1].size())
2969 r_class = cp.retrieval_classes[k - 1][cls - 1];
2970 // A returning retrieval not recorded in the bitmap is an unreachable
2971 // artifact of the enumeration; it is not continued.
2972 if (from_retrieval && !in_flight) continue;
2973
2974 if (!from_retrieval && r_class != 0) {
2975 // BEGIN a fetch, or MERGE onto one already running. The merge is
2976 // the delayed hit: it adds nothing to the cache's server block,
2977 // and is held in block B until the fetch completes and releases
2978 // it in the hit class of the class that issued it.
2979 std::vector<T> srv_b = srv_e;
2980 std::vector<T> var_b = var;
2981 if (!in_flight) {
2982 srv_b[r_class - 1] += one;
2983 if (tcc + k <= var_b.size()) var_b[tcc + k - 1] = one;
2984 } else {
2985 if (width_b == 0) continue;
2986 std::size_t bslot = width_b;
2987 for (std::size_t j = 0; j < rc_list.size(); ++j)
2988 if (rc_list[j] == r_class) { bslot = j; break; }
2989 if (bslot >= width_b) continue;
2990 double pend = 0;
2991 for (std::size_t j = 0; j < width_b; ++j)
2992 pend += num_traits<T>::to_double(var_b[block_b + j]);
2993 if (pend >= maxpend) continue; // beyond the truncation level
2994 var_b[block_b + bslot] += one;
2995 }
2996 std::vector<T> row = srv_b;
2997 row.insert(row.end(), var_b.begin(), var_b.end());
2998 out.space.push_back(row);
2999 out.rate.push_back(T(p[k - 1] * imm));
3000 out.prob.push_back(one);
3001 continue;
3002 }
3003
3004 // The fetch is complete (or there is no retrieval system): the job
3005 // leaves in the miss class, the item may be admitted into one of the
3006 // lists, and every request merged onto this fetch is released in the
3007 // SAME transition as a delayed hit.
3008 if (cls - 1 >= cp.missclass.size() || cp.missclass[cls - 1] == 0) continue;
3009 std::vector<T> srv_m = srv_e;
3010 srv_m[cp.missclass[cls - 1] - 1] += one;
3011 std::vector<T> var_m = var;
3012 if (tcc + k <= var_m.size()) var_m[tcc + k - 1] = zero; // retrieval done
3013 for (std::size_t j = 0; j < width_b; ++j) {
3014 if (rc_items[j] != k) continue;
3015 const double held = num_traits<T>::to_double(var_m[block_b + j]);
3016 if (held <= 0) continue;
3017 const std::size_t oc = rc_orig[j];
3018 if (oc - 1 >= cp.hitclass.size() || cp.hitclass[oc - 1] == 0) continue;
3019 srv_m[cp.hitclass[oc - 1] - 1] += var_m[block_b + j];
3020 var_m[block_b + j] = zero;
3021 }
3022
3023 // Column 1 of the access cost is the REJECT branch: the item passes
3024 // through without being cached at all.
3025 const T rej = have_ac ? ac(0, 0) : zero;
3026 if (num_traits<T>::to_double(rej) > 0) {
3027 std::vector<T> row = srv_m;
3028 row.insert(row.end(), var_m.begin(), var_m.end());
3029 out.space.push_back(row);
3030 out.rate.push_back(T(rej * p[k - 1] * imm));
3031 out.prob.push_back(one);
3032 }
3033 for (std::size_t l = 1; l <= h; ++l) {
3034 const T w = have_ac ? ac(0, l) : (l == 1 ? one : zero);
3035 if (num_traits<T>::to_double(w) <= 0) continue;
3036 if (m[l - 1] <= 0) continue;
3037 const std::size_t ml = static_cast<std::size_t>(m[l - 1]);
3039 // Random replacement: the item lands uniformly in any slot.
3040 for (std::size_t rr = 1; rr <= ml; ++rr) {
3041 std::vector<T> vp = var_m;
3042 vp[cpos(l, rr)] = num_traits<T>::from_int(static_cast<long>(k));
3043 std::vector<T> row = srv_m;
3044 row.insert(row.end(), vp.begin(), vp.end());
3045 out.space.push_back(row);
3046 out.rate.push_back(T(w * p[k - 1] /
3047 num_traits<T>::from_int(static_cast<long>(ml)) * imm));
3048 out.prob.push_back(one);
3049 }
3050 } else {
3051 // The ordered families insert at the HEAD, shifting the list
3052 // down by one and evicting its tail.
3053 std::vector<T> vp = var_m;
3054 for (std::size_t j = ml; j >= 2; --j) vp[cpos(l, j)] = var_m[cpos(l, j - 1)];
3055 vp[cpos(l, 1)] = num_traits<T>::from_int(static_cast<long>(k));
3056 T rate = T(w * p[k - 1] * imm);
3057 // q-LRU admits a miss only with probability q; the rest
3058 // passes through uncached.
3060 const T q = cp.qlru;
3061 if (num_traits<T>::to_double(q) < 1) {
3062 std::vector<T> row0 = srv_m;
3063 row0.insert(row0.end(), var_m.begin(), var_m.end());
3064 out.space.push_back(row0);
3065 out.rate.push_back(T(rate * T(one - q)));
3066 out.prob.push_back(one);
3067 }
3068 rate = T(rate * q);
3069 }
3070 if (num_traits<T>::to_double(rate) <= 0) continue;
3071 std::vector<T> row = srv_m;
3072 row.insert(row.end(), vp.begin(), vp.end());
3073 out.space.push_back(row);
3074 out.rate.push_back(rate);
3075 out.prob.push_back(one);
3076 }
3077 }
3078 } else {
3079 // CACHE HIT: the job leaves in the hit class. Which list it was
3080 // found in decides how the contents move.
3081 if (cls - 1 >= cp.hitclass.size() || cp.hitclass[cls - 1] == 0) continue;
3082 std::vector<T> srv_h = srv_e;
3083 srv_h[cp.hitclass[cls - 1] - 1] += one;
3084 std::size_t li = 1, acc = 0;
3085 for (std::size_t t = 0; t < h; ++t) {
3086 const std::size_t mt = m[t] > 0 ? static_cast<std::size_t>(m[t]) : 0;
3087 if (posk <= acc + mt) { li = t + 1; break; }
3088 acc += mt;
3089 }
3090 const std::size_t j = posk - acc;
3091 if (li < h) {
3092 // A HIT BELOW THE TERMINAL LIST PROMOTES, and that is what makes
3093 // an h-list cache more than h caches side by side. Row `li` of
3094 // the access cost routes the item from list li into list
3095 // inew >= li, exactly as `afterEventCache.m` does over `inew =
3096 // i:h`; an empty accost is the reference default
3097 // `diag(ones(1,h),1)` with a 1 in the bottom-right, the linear
3098 // cache that moves the item one list up. Omitting this branch
3099 // froze every list above the first at its initial contents: on
3100 // m=[2,1] only 12 of the 60 configurations stayed reachable, and
3101 // the CTMC hit ratio of cache_compare_replc came out 4.9% low.
3102 for (std::size_t inew = li; inew <= h; ++inew) {
3103 if (m[inew - 1] <= 0) continue;
3104 const std::size_t mn = static_cast<std::size_t>(m[inew - 1]);
3105 const T w = have_ac ? ac(li, inew) : (inew == li + 1 ? one : zero);
3106 if (num_traits<T>::to_double(w) <= 0) continue;
3108 // Random replacement swaps with a uniformly drawn slot.
3109 for (std::size_t r = 1; r <= mn; ++r) {
3110 std::vector<T> vp = var;
3111 vp[cpos(li, j)] = var[cpos(inew, r)];
3112 vp[cpos(inew, r)] = num_traits<T>::from_int(static_cast<long>(k));
3113 std::vector<T> row = srv_h;
3114 row.insert(row.end(), vp.begin(), vp.end());
3115 out.space.push_back(row);
3116 out.rate.push_back(
3117 T(w * p[k - 1] /
3118 num_traits<T>::from_int(static_cast<long>(mn)) * imm));
3119 out.prob.push_back(one);
3120 }
3121 continue;
3122 }
3123 // Every read below is of the UNMODIFIED row, so the three
3124 // moves compose in the reference's order even where the
3125 // source list and the target list are the same one.
3126 std::vector<T> vp = var;
3127 // The LRU family closes the gap in list li; FIFO orders by
3128 // insertion and so leaves list li otherwise untouched.
3129 const bool ordered = cp.replacestrat != lang::ReplacementStrategy::FIFO;
3130 if (ordered)
3131 for (std::size_t t = j; t >= 2; --t) vp[cpos(li, t)] = var[cpos(li, t - 1)];
3132 // The tail evicted from the target list takes the slot the
3133 // promoted item vacated.
3134 vp[cpos(li, ordered ? 1 : j)] = var[cpos(inew, mn)];
3135 for (std::size_t t = mn; t >= 2; --t) vp[cpos(inew, t)] = var[cpos(inew, t - 1)];
3136 vp[cpos(inew, 1)] = num_traits<T>::from_int(static_cast<long>(k));
3137 std::vector<T> row = srv_h;
3138 row.insert(row.end(), vp.begin(), vp.end());
3139 out.space.push_back(row);
3140 out.rate.push_back(T(w * p[k - 1] * imm));
3141 out.prob.push_back(one);
3142 }
3146 // A hit in the terminal list does not reorder these: FIFO orders
3147 // by INSERTION, and random replacement has no order to disturb.
3148 std::vector<T> row = srv_h;
3149 row.insert(row.end(), var.begin(), var.end());
3150 out.space.push_back(row);
3151 out.rate.push_back(T(p[k - 1] * imm));
3152 out.prob.push_back(one);
3153 } else {
3154 // LRU and its relatives promote the hit item to the head of its
3155 // list, which is the whole content of "recently used".
3156 std::vector<T> vp = var;
3157 for (std::size_t t = j; t >= 2; --t) vp[cpos(li, t)] = var[cpos(li, t - 1)];
3158 vp[cpos(li, 1)] = var[cpos(li, j)];
3159 std::vector<T> row = srv_h;
3160 row.insert(row.end(), vp.begin(), vp.end());
3161 out.space.push_back(row);
3162 out.rate.push_back(T(p[k - 1] * imm));
3163 out.prob.push_back(one);
3164 }
3165 }
3166 }
3167 return out;
3168}
3169
3170
3171/** One half of a GLOBAL synchronization: a mode event at a node. */
3172template <class T>
3174 EventType event = EventType::LOCAL;
3175 std::size_t node = 0; ///< 1-based node index (a Transition, or a place)
3176 std::size_t mode = 0; ///< 1-based mode index
3177 std::size_t cls = 1; ///< 1-based class the arc moves
3178 T weight = num_traits<T>::from_int(1); ///< arc multiplicity
3179};
3180
3181/**
3182 * A GLOBAL synchronization: an SPN mode event and the place arcs it drives.
3183 *
3184 * Unlike an ordinary Sync, which pairs ONE active with ONE passive, a firing
3185 * touches every input and output place at once -- that atomicity is what makes
3186 * a Petri net transition a transition. PRE passives consume, POST produce, and
3187 * LOCAL passives are read-only (an inhibitor place, whose marking is tested but
3188 * never moved).
3189 */
3190template <class T>
3193 std::vector<ModeEvent<T>> passive;
3194};
3195
3196/**
3197 * Port of `MNetwork.refreshGlobalSync`: the ENABLE and FIRE synchronizations.
3198 *
3199 * An inhibiting place enters as a LOCAL passive rather than a PRE, and only
3200 * when it is not already an enabling or firing place: its marking is read for
3201 * the inhibition test but no token crosses the arc.
3202 */
3203template <class T>
3204std::vector<GlobalSync<T>> refresh_global_sync(const NetworkStruct<T>& sn) {
3205 std::vector<GlobalSync<T>> gsync;
3206 const T one = num_traits<T>::from_int(1);
3207 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
3208 if (sn.nodes[ind - 1].nodetype != NodeType::Transition) continue;
3209 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
3210 sn.transparam.find(ind);
3211 if (it == sn.transparam.end()) continue;
3212 const TransitionParam<T>& tp = it->second;
3213 for (int pass = 0; pass < 2; ++pass) {
3214 for (std::size_t m = 1; m <= tp.nmodes; ++m) {
3215 // ONE ENTRY PER (place, class) ARC, which is what makes a
3216 // multiclass net a different net from the class-summed one: a
3217 // Class2 token at a place must not satisfy a Class1 pre-arc, so
3218 // the pair travels into the passive rather than the place alone.
3219 std::vector<std::pair<std::size_t, std::size_t>> enab, fire, inhib;
3220 const Matrix<T>& en = tp.enabling[m - 1];
3221 const Matrix<T>& fi = tp.firing[m - 1];
3222 const Matrix<T>& ih = tp.inhibiting[m - 1];
3223 for (std::size_t q = 0; q < en.rows(); ++q)
3224 for (std::size_t r = 0; r < en.cols(); ++r)
3225 if (num_traits<T>::to_double(en(q, r)) > 0)
3226 enab.push_back(std::make_pair(q + 1, r + 1));
3227 for (std::size_t q = 0; q < fi.rows(); ++q)
3228 for (std::size_t r = 0; r < fi.cols(); ++r)
3229 if (num_traits<T>::to_double(fi(q, r)) > 0)
3230 fire.push_back(std::make_pair(q + 1, r + 1));
3231 for (std::size_t q = 0; q < ih.rows(); ++q)
3232 for (std::size_t r = 0; r < ih.cols(); ++r) {
3233 if (std::isinf(num_traits<T>::to_double(ih(q, r)))) continue;
3234 // The de-duplication is against the PLACE, not the
3235 // (place, class) pair: the passive's job is to bring the
3236 // place's marginal into the outcome, and one copy of a
3237 // place carries every class of it.
3238 bool dup = false;
3239 for (std::size_t i = 0; i < enab.size(); ++i)
3240 dup = dup || enab[i].first == q + 1;
3241 for (std::size_t i = 0; i < fire.size(); ++i)
3242 dup = dup || fire[i].first == q + 1;
3243 for (std::size_t i = 0; i < inhib.size(); ++i)
3244 dup = dup || inhib[i].first == q + 1;
3245 if (!dup) inhib.push_back(std::make_pair(q + 1, r + 1));
3246 }
3247 GlobalSync<T> g;
3248 g.active.event = pass == 0 ? EventType::ENABLE : EventType::FIRE;
3249 g.active.node = ind;
3250 g.active.mode = m;
3251 if (pass == 0) {
3252 // An ENABLE only READS the markings, so every passive is
3253 // LOCAL: enabling it is a test, not a token movement. One
3254 // per PLACE here, since a read of a place reads every class.
3255 std::vector<std::size_t> seen;
3256 auto once = [&](std::size_t q, std::size_t r) {
3257 for (std::size_t i = 0; i < seen.size(); ++i)
3258 if (seen[i] == q) return;
3259 seen.push_back(q);
3260 g.passive.push_back(ModeEvent<T>{EventType::LOCAL, q, m, r, one});
3261 };
3262 for (std::size_t i = 0; i < enab.size(); ++i) once(enab[i].first, enab[i].second);
3263 for (std::size_t i = 0; i < inhib.size(); ++i)
3264 once(inhib[i].first, inhib[i].second);
3265 } else {
3266 for (std::size_t i = 0; i < enab.size(); ++i)
3267 g.passive.push_back(ModeEvent<T>{EventType::PRE, enab[i].first, m,
3268 enab[i].second,
3269 en(enab[i].first - 1, enab[i].second - 1)});
3270 for (std::size_t i = 0; i < fire.size(); ++i)
3271 g.passive.push_back(ModeEvent<T>{EventType::POST, fire[i].first, m,
3272 fire[i].second,
3273 fi(fire[i].first - 1, fire[i].second - 1)});
3274 for (std::size_t i = 0; i < inhib.size(); ++i)
3275 g.passive.push_back(ModeEvent<T>{EventType::LOCAL, inhib[i].first, m,
3276 inhib[i].second, one});
3277 }
3278 gsync.push_back(g);
3279 }
3280 }
3281 }
3282 return gsync;
3283}
3284
3285/** What one global event produces: a whole network state per outcome. */
3286template <class T>
3288 std::vector<NetState<T>> space;
3289 std::vector<T> rate, prob;
3290 /**
3291 * True where the outcome is a firing COMPLETION, i.e. one that applied the
3292 * PRE/POST updates. Callers must NOT re-derive this from the markings: a
3293 * transition whose firing returns exactly what its enabling consumed
3294 * leaves every marking invariant yet still completed.
3295 */
3296 std::vector<bool> completion;
3297 bool empty() const { return space.empty(); }
3298};
3299
3300/**
3301 * Port of `State.afterGlobalEvent`: an SPN mode ENABLEs or FIREs.
3302 *
3303 * This is the one handler that rewrites SEVERAL nodes at once, because a
3304 * firing is atomic across all its arcs. The Transition's own row records how
3305 * many servers of each mode are idle, running (and in which firing phase), and
3306 * have just fired; the places are rewritten through the PRE and POST passives.
3307 */
3308template <class T>
3310 const GlobalSync<T>& gl) {
3311 const std::size_t R = sn.nclasses;
3312 const std::size_t ind = gl.active.node;
3313 const std::size_t mode = gl.active.mode;
3314 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
3316 GlobalOutcome<T> out;
3317 const std::size_t isf = sn.stateful_index(ind);
3318 if (isf == 0) throw InputError("after_global_event: the transition is not stateful");
3319 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator it =
3320 sn.transparam.find(ind);
3321 if (it == sn.transparam.end()) throw InputError("after_global_event: node has no TransitionParam");
3322 const TransitionParam<T>& tp = it->second;
3323
3324 std::vector<std::size_t> fK(tp.nmodes, 1), fKs(tp.nmodes, 0);
3325 std::size_t tot = 0;
3326 for (std::size_t m = 0; m < tp.nmodes; ++m) {
3327 fK[m] = m < tp.firingphases.size() && tp.firingphases[m] > 0 ? tp.firingphases[m] : 1;
3328 fKs[m] = tot;
3329 tot += fK[m];
3330 }
3331 const std::vector<T>& row = glspace.local[isf - 1];
3332 std::vector<T> buf(row.begin(), row.begin() + tp.nmodes);
3333 std::vector<T> srv(row.begin() + tp.nmodes, row.begin() + tp.nmodes + tot);
3334 std::vector<T> fired(row.begin() + tp.nmodes + tot,
3335 row.begin() + 2 * tp.nmodes + tot);
3336 const std::vector<T> var(row.begin() + 2 * tp.nmodes + tot, row.end());
3337
3338 // The marking of every place this mode reads, node-indexed.
3339 std::vector<std::vector<T>> ep(sn.nodes.size() + 1, std::vector<T>(R, zero));
3340 for (std::size_t j = 0; j < gl.passive.size(); ++j) {
3341 const std::size_t pn = gl.passive[j].node;
3342 const std::size_t pisf = sn.stateful_index(pn);
3343 if (pisf == 0) continue;
3344 const std::pair<T, std::vector<T>> mg = to_marginal_aggr(sn, pn, glspace.local[pisf - 1]);
3345 ep[pn] = mg.second;
3346 }
3347
3348 // The enabling DEGREE: how many concurrent firings the marking supports.
3349 // An inhibitor arc disables the mode outright once its threshold is met.
3350 //
3351 // EVERY TEST IS PER (place, class), the elementwise comparison
3352 // `afterGlobalEvent.m:85` makes against `enabling_m`. Summing the marking
3353 // over classes first, as this port did until 2026-08-12, let a Class2 token
3354 // satisfy a Class1 pre-arc: on a net where Mode1 needs two Class1 tokens at
3355 // P1 and Mode2 one Class2 token there, the summed test fires Mode1 off a
3356 // marking that holds no Class1 token at all.
3357 const Matrix<T>& en_m = tp.enabling[mode - 1];
3358 const Matrix<T>& ih_m = tp.inhibiting[mode - 1];
3359 bool inhibited = false;
3360 for (std::size_t q = 0; q < ih_m.rows(); ++q)
3361 for (std::size_t r = 0; r < ih_m.cols() && r < R; ++r) {
3362 const double thr = num_traits<T>::to_double(ih_m(q, r));
3363 if (std::isinf(thr)) continue;
3364 if (num_traits<T>::to_double(ep[q + 1][r]) >= thr) inhibited = true;
3365 }
3366 bool under = false;
3367 for (std::size_t q = 0; q < en_m.rows(); ++q)
3368 for (std::size_t r = 0; r < en_m.cols() && r < R; ++r) {
3369 const double need = num_traits<T>::to_double(en_m(q, r));
3370 if (need <= 0) continue;
3371 if (num_traits<T>::to_double(ep[q + 1][r]) < need) under = true;
3372 }
3373 long mark_degree = 0;
3374 if (!inhibited && !under) {
3375 long d = 1;
3376 for (;;) {
3377 bool ok = true;
3378 for (std::size_t q = 0; q < en_m.rows() && ok; ++q)
3379 for (std::size_t r = 0; r < en_m.cols() && r < R && ok; ++r) {
3380 const double need = num_traits<T>::to_double(en_m(q, r)) * d;
3381 if (need <= 0) continue;
3382 if (num_traits<T>::to_double(ep[q + 1][r]) < need) ok = false;
3383 }
3384 if (!ok) break;
3385 ++d;
3386 }
3387 mark_degree = d - 1;
3388 }
3389 const double svm = mode - 1 < tp.nmodeservers.size() ? tp.nmodeservers[mode - 1] : 1.0;
3390 const long nsrv = std::isfinite(svm) ? static_cast<long>(svm)
3391 : static_cast<long>(GlobalConstants::MaxInt);
3392
3393 if (gl.active.event == EventType::ENABLE) {
3394 long running = 0;
3395 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3396 running += static_cast<long>(num_traits<T>::to_double(srv[fKs[mode - 1] + k]));
3397 if (inhibited || under) {
3398 // Disabled: every server of this mode returns to the idle pool.
3399 std::vector<T> b2 = buf, s2 = srv;
3400 b2[mode - 1] = num_traits<T>::from_int(nsrv);
3401 for (std::size_t k = 0; k < fK[mode - 1]; ++k) s2[fKs[mode - 1] + k] = zero;
3402 std::vector<T> nr = b2;
3403 nr.insert(nr.end(), s2.begin(), s2.end());
3404 nr.insert(nr.end(), fired.begin(), fired.end());
3405 nr.insert(nr.end(), var.begin(), var.end());
3406 if (nr == row) return out; // already disabled: not a transition
3407 NetState<T> ns = glspace;
3408 ns.local[isf - 1] = nr;
3409 out.space.push_back(ns);
3410 out.rate.push_back(imm);
3411 out.prob.push_back(one);
3412 out.completion.push_back(false);
3413 return out;
3414 }
3415 const long want = std::min(mark_degree, nsrv);
3416 if (running == want) return out; // nothing to do
3417 if (running < want) {
3418 // Start servers, distributing them over the firing phases by the
3419 // entry law; the multinomial weight is the probability of that
3420 // split.
3421 const long nadd = want - running;
3422 std::vector<T> pe(fK[mode - 1], zero);
3423 if (mode - 1 < tp.firingproc.size() && tp.firingproc[mode - 1].D0.rows() ==
3424 static_cast<std::size_t>(fK[mode - 1])) {
3425 mam::Map<T> mp;
3426 mp.D0 = tp.firingproc[mode - 1].D0;
3427 mp.D1 = tp.firingproc[mode - 1].D1;
3428 const std::vector<T> pv = mam::map_pie(mp);
3429 for (std::size_t k = 0; k < fK[mode - 1]; ++k) pe[k] = pv[k];
3430 } else {
3431 pe[0] = one;
3432 }
3433 // Enumerate the splits of nadd over the phases.
3434 std::vector<std::vector<long>> combs;
3435 std::vector<long> cur(fK[mode - 1], 0);
3436 std::function<void(std::size_t, long)> rec = [&](std::size_t k, long left) {
3437 if (k + 1 == fK[mode - 1]) {
3438 cur[k] = left;
3439 combs.push_back(cur);
3440 return;
3441 }
3442 for (long v = left; v >= 0; --v) {
3443 cur[k] = v;
3444 rec(k + 1, left - v);
3445 }
3446 };
3447 rec(0, nadd);
3448 for (std::size_t i = 0; i < combs.size(); ++i) {
3449 std::vector<T> b2 = buf, s2 = srv;
3450 b2[mode - 1] -= num_traits<T>::from_int(nadd);
3451 double logp = std::lgamma(static_cast<double>(nadd) + 1.0);
3452 bool zeroprob = false;
3453 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
3454 s2[fKs[mode - 1] + k] += num_traits<T>::from_int(combs[i][k]);
3455 const double pk = num_traits<T>::to_double(pe[k]);
3456 if (pk > 0)
3457 logp += combs[i][k] * std::log(pk) -
3458 std::lgamma(static_cast<double>(combs[i][k]) + 1.0);
3459 else if (combs[i][k] > 0)
3460 zeroprob = true;
3461 }
3462 std::vector<T> nr = b2;
3463 nr.insert(nr.end(), s2.begin(), s2.end());
3464 nr.insert(nr.end(), fired.begin(), fired.end());
3465 nr.insert(nr.end(), var.begin(), var.end());
3466 NetState<T> ns = glspace;
3467 ns.local[isf - 1] = nr;
3468 out.space.push_back(ns);
3469 out.rate.push_back(imm);
3470 out.prob.push_back(zeroprob ? zero : num_traits<T>::from_double(std::exp(logp)));
3471 out.completion.push_back(false);
3472 }
3473 return out;
3474 }
3475 // Stop the surplus servers, chosen uniformly across the phases: the
3476 // weight is the multivariate hypergeometric probability of that choice.
3477 const long ndiff = running - want;
3478 std::vector<long> sv(fK[mode - 1], 0);
3479 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3480 sv[k] = static_cast<long>(num_traits<T>::to_double(srv[fKs[mode - 1] + k]));
3481 std::vector<std::vector<long>> combs;
3482 std::vector<long> cur(fK[mode - 1], 0);
3483 std::function<void(std::size_t, long)> rec = [&](std::size_t k, long left) {
3484 if (k + 1 == fK[mode - 1]) {
3485 if (left > sv[k]) return;
3486 cur[k] = left;
3487 combs.push_back(cur);
3488 return;
3489 }
3490 for (long v = std::min(left, sv[k]); v >= 0; --v) {
3491 cur[k] = v;
3492 rec(k + 1, left - v);
3493 }
3494 };
3495 rec(0, ndiff);
3496 std::vector<double> w(combs.size(), 0.0);
3497 double wmax = -1e300;
3498 for (std::size_t i = 0; i < combs.size(); ++i) {
3499 double lw = 0;
3500 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3501 lw += std::lgamma(static_cast<double>(sv[k]) + 1.0) -
3502 std::lgamma(static_cast<double>(combs[i][k]) + 1.0) -
3503 std::lgamma(static_cast<double>(sv[k] - combs[i][k]) + 1.0);
3504 w[i] = lw;
3505 wmax = std::max(wmax, lw);
3506 }
3507 double wsum = 0;
3508 for (std::size_t i = 0; i < w.size(); ++i) {
3509 w[i] = std::exp(w[i] - wmax);
3510 wsum += w[i];
3511 }
3512 for (std::size_t i = 0; i < combs.size(); ++i) {
3513 std::vector<T> b2 = buf, s2 = srv;
3514 for (std::size_t k = 0; k < fK[mode - 1]; ++k)
3515 s2[fKs[mode - 1] + k] = num_traits<T>::from_int(sv[k] - combs[i][k]);
3516 b2[mode - 1] += num_traits<T>::from_int(ndiff);
3517 std::vector<T> nr = b2;
3518 nr.insert(nr.end(), s2.begin(), s2.end());
3519 nr.insert(nr.end(), fired.begin(), fired.end());
3520 nr.insert(nr.end(), var.begin(), var.end());
3521 NetState<T> ns = glspace;
3522 ns.local[isf - 1] = nr;
3523 out.space.push_back(ns);
3524 out.rate.push_back(imm);
3525 out.prob.push_back(num_traits<T>::from_double(wsum > 0 ? w[i] / wsum : 0.0));
3526 out.completion.push_back(false);
3527 }
3528 return out;
3529 }
3530
3531 if (gl.active.event != EventType::FIRE) return out;
3532
3533 const bool immediate_mode = mode - 1 < tp.timing.size() &&
3535 const T fw = immediate_mode && mode - 1 < tp.fireweight.size() ? tp.fireweight[mode - 1] : one;
3536 const long en_degree = inhibited ? 0 : std::min(mark_degree, nsrv);
3537 const long imm_servers = immediate_mode ? std::min(mark_degree, nsrv) : 0;
3538
3539 for (std::size_t k = 0; k < fK[mode - 1]; ++k) {
3540 const double in_k = num_traits<T>::to_double(srv[fKs[mode - 1] + k]);
3541 const bool fires = immediate_mode ? (k == 0 && imm_servers >= 1)
3542 : (in_k > 0 && en_degree >= 1);
3543 if (!fires) continue;
3544 T rate = zero;
3545 if (immediate_mode) {
3546 rate = T(imm * fw * num_traits<T>::from_int(imm_servers));
3547 } else {
3548 T d1sum = zero;
3549 if (mode - 1 < tp.firingproc.size())
3550 for (std::size_t j = 0; j < tp.firingproc[mode - 1].D1.cols(); ++j)
3551 d1sum += tp.firingproc[mode - 1].D1(k, j);
3552 rate = T(d1sum * num_traits<T>::from_double(in_k));
3553 // A marking-dependent firing rate g_mode(marking) is exact here,
3554 // because the CTMC evaluates it per enumerated state.
3555 if (mode - 1 < tp.firingdep.size() && tp.firingdep[mode - 1]) {
3556 std::vector<T> mk;
3557 for (std::size_t q = 1; q <= sn.nodes.size(); ++q) {
3558 T s2 = zero;
3559 for (std::size_t r = 0; r < R; ++r) s2 += ep[q][r];
3560 mk.push_back(s2);
3561 }
3562 rate = T(rate * tp.firingdep[mode - 1](mk));
3563 }
3564 }
3565 if (num_traits<T>::to_double(rate) <= 0) continue;
3566
3567 std::vector<T> b2 = buf, s2 = srv;
3568 if (in_k > 0) {
3569 s2[fKs[mode - 1] + k] -= one; // the firing server leaves execution
3570 b2[mode - 1] += one; // and returns to the idle pool
3571 }
3572 NetState<T> ns = glspace;
3573 std::vector<T> nr = b2;
3574 nr.insert(nr.end(), s2.begin(), s2.end());
3575 nr.insert(nr.end(), fired.begin(), fired.end());
3576 nr.insert(nr.end(), var.begin(), var.end());
3577 ns.local[isf - 1] = nr;
3578
3579 // The arcs fire ATOMICALLY with the mode: PRE consumes from every
3580 // input place and POST produces into every output place, in one
3581 // transition. Splitting them would let the net occupy a state in which
3582 // the tokens have left one place and not arrived at the other.
3583 for (std::size_t j = 0; j < gl.passive.size(); ++j) {
3584 const ModeEvent<T>& pe2 = gl.passive[j];
3585 if (pe2.event != EventType::PRE && pe2.event != EventType::POST) continue;
3586 const std::size_t pisf = sn.stateful_index(pe2.node);
3587 if (pisf == 0) continue;
3588 std::vector<T>& prow = ns.local[pisf - 1];
3589 const double wgt = num_traits<T>::to_double(pe2.weight);
3590 const std::size_t pist = sn.nodes[pe2.node - 1].station;
3591 const SchedStrategy psched =
3592 pist != 0 ? sn.stations[pist - 1].sched : SchedStrategy::INF;
3593 const std::size_t c = pe2.cls;
3594 if (pe2.event == EventType::PRE) {
3595 if (state_detail::buffer_is_class_tag(psched)) {
3596 // An ordered buffer: consume from the head end, matching
3597 // the discipline rather than a count.
3598 long left = static_cast<long>(wgt);
3599 const T tag = num_traits<T>::from_int(static_cast<long>(c));
3600 if (psched == SchedStrategy::LCFS) {
3601 for (std::size_t b = 0; b < prow.size() && left > 0; ++b)
3602 if (prow[b] == tag) { prow[b] = zero; --left; }
3603 } else {
3604 for (std::size_t b = prow.size(); b-- > 0 && left > 0;)
3605 if (prow[b] == tag) { prow[b] = zero; --left; }
3606 }
3607 } else if (prow.size() > R) {
3608 // A Place with a [count | server] split: drain the server
3609 // slot into the count, as the reference does.
3610 const double totc = num_traits<T>::to_double(prow[c - 1]) +
3611 num_traits<T>::to_double(prow[R + c - 1]);
3612 prow[c - 1] = num_traits<T>::from_double(totc - wgt);
3613 prow[R + c - 1] = zero;
3614 } else if (c - 1 < prow.size()) {
3615 prow[c - 1] -= num_traits<T>::from_double(wgt);
3616 }
3617 } else {
3618 if (state_detail::buffer_is_class_tag(psched)) {
3619 for (long q = 0; q < static_cast<long>(wgt); ++q)
3620 prow.insert(prow.begin(), num_traits<T>::from_int(static_cast<long>(c)));
3621 } else if (c - 1 < prow.size()) {
3622 prow[c - 1] += num_traits<T>::from_double(wgt);
3623 }
3624 }
3625 }
3626 out.space.push_back(ns);
3627 out.rate.push_back(rate);
3628 out.prob.push_back(one);
3629 out.completion.push_back(true);
3630 }
3631 return out;
3632}
3633
3634/** One half of a synchronization: an event at a node, in a class. */
3635template <class T>
3637 EventType event = EventType::LOCAL;
3638 std::size_t node = 0; ///< 1-based node index, or `local` for the dummy
3639 std::size_t cls = 0; ///< 1-based class index
3640 T prob = num_traits<T>::from_int(1); ///< routing probability, passive half
3641 /**
3642 * The routing probability is a FUNCTION of the network state, so `prob`
3643 * holds only the state-independent placeholder and the generator must read
3644 * `rt_state(sn, state)(rt_row, rt_col)` instead.
3645 *
3646 * The reference decides this per ACTIVE NODE (`sn.isstatedep(node_a,3)`) and
3647 * then calls whatever `prob` holds, which throws where a node routes one
3648 * class state-dependently and another by probability. Deciding it per
3649 * SYNCHRONIZATION agrees wherever the reference runs at all, and does not
3650 * throw where it would.
3651 */
3652 bool statedep = false;
3653 std::size_t rt_row = 0; ///< (isf-1)*nclasses + (r-1) of the active half
3654 std::size_t rt_col = 0; ///< (jsf-1)*nclasses + (s-1) of this passive half
3655};
3656
3657/**
3658 * One synchronization: an ACTIVE event and the PASSIVE event it drives.
3659 *
3660 * Every transition of the CTMC is one of these. The active half sets the rate;
3661 * the passive half is where the job lands, weighted by the routing probability.
3662 * A LOCAL passive half means the active event moves no job out of its node --
3663 * a phase change, a reneging job leaving the system, a server failing.
3664 */
3665template <class T>
3669
3670/**
3671 * Port of `MNetwork.refreshSync`: the synchronization list.
3672 *
3673 * The ORDER matters as much as the content: the generator adds rates in this
3674 * order, and while addition is commutative in exact arithmetic it is not in
3675 * floating point, so a reordered list perturbs the last digits of every
3676 * reported metric.
3677 *
3678 * @param impatience_classes (station x class) true where reneging is declared
3679 * @param breakdown_nodes 1-based node indices with a server breakdown
3680 * @param sn the refreshed network struct
3681 */
3682template <class T>
3683std::vector<Sync<T>> refresh_sync(
3684 const NetworkStruct<T>& sn,
3685 const std::vector<std::vector<bool>>& impatience_classes = std::vector<std::vector<bool>>(),
3686 const std::vector<std::size_t>& breakdown_nodes = std::vector<std::size_t>()) {
3687 const std::size_t R = sn.nclasses;
3688 const std::size_t local = sn.nodes.size() + 1; // the dummy passive node
3689 const T one = num_traits<T>::from_int(1);
3690 std::vector<Sync<T>> sync;
3691
3692 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
3693 const NodeDef& nd = sn.nodes[ind - 1];
3694 const std::size_t ist = nd.station;
3695 for (std::size_t r = 1; r <= R; ++r) {
3696 // A phase-change action exists only for a multi-phase service:
3697 // with one phase there is no internal transition to make.
3698 if (ist != 0 && sn.phases_of(ist, r) > 1) {
3699 Sync<T> s;
3700 s.active = SyncEvent<T>{EventType::PHASE, ind, r, one};
3701 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3702 sync.push_back(s);
3703 }
3704 if (ist != 0 && impatience_classes.size() >= ist &&
3705 impatience_classes[ist - 1].size() >= r && impatience_classes[ist - 1][r - 1]) {
3706 Sync<T> s;
3707 s.active = SyncEvent<T>{EventType::RENEGE, ind, r, one};
3708 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3709 sync.push_back(s);
3710 }
3711 if (ist != 0) {
3712 const typename std::map<std::size_t, RetrialParam<T>>::const_iterator rit =
3713 sn.retrialparam.find(ist);
3714 if (rit != sn.retrialparam.end() && rit->second.retrial_proc.size() >= r &&
3715 !rit->second.retrial_proc[r - 1].disabled) {
3716 Sync<T> s;
3717 s.active = SyncEvent<T>{EventType::RETRY, ind, r, one};
3718 s.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3719 sync.push_back(s);
3720 }
3721 }
3722 // Failure and repair are properties of the SERVER, not of a class,
3723 // so exactly one pair is emitted per station rather than one per
3724 // class -- hence the r == 1 guard.
3725 if (ist != 0 && r == 1) {
3726 bool has_bd = false;
3727 for (std::size_t b = 0; b < breakdown_nodes.size(); ++b)
3728 if (breakdown_nodes[b] == ind) { has_bd = true; break; }
3729 if (has_bd) {
3730 Sync<T> f, rp;
3731 f.active = SyncEvent<T>{EventType::FAILURE, ind, r, one};
3732 f.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3733 sync.push_back(f);
3734 rp.active = SyncEvent<T>{EventType::REPAIR, ind, r, one};
3735 rp.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3736 sync.push_back(rp);
3737 }
3738 }
3739 // A polling station needs a SWITCH action per buffer whose
3740 // entering switchover is a real (non-immediate) walk.
3741 if (ist != 0 && sn.stations[ist - 1].sched == SchedStrategy::POLLING) {
3742 const PollingInfo<T> pinfo = polling_info(sn, ind);
3743 if (pinfo.valid && pinfo.has_sw[r - 1]) {
3744 Sync<T> s2;
3745 s2.active = SyncEvent<T>{EventType::SWITCH, ind, r, one};
3746 s2.passive = SyncEvent<T>{EventType::LOCAL, local, r, one};
3747 sync.push_back(s2);
3748 }
3749 }
3750 if (!nd.stateful) continue;
3751 // A stateful Fork emits no departure sync: the atomic multi-branch
3752 // emission is a fork firing synchronization instead.
3753 if (nd.nodetype == NodeType::Fork) continue;
3754
3755 // A CACHE READ IS ITS OWN ACTION, not a routing decision. The read
3756 // consults the contents, rewrites them and switches the job into the
3757 // hit or the miss class, so it moves no job between nodes and its
3758 // passive half is the dummy.
3759 //
3760 // THE READ CLASS THEREFORE EMITS NO DEPARTURE HERE. `refresh_routing`
3761 // resolves its unresolved cache split to a uniform half-half so the
3762 // VISIT equations have a number; the reference leaves the same two
3763 // entries NaN, and `ceil(NaN) > 0` is false, so no departure sync is
3764 // built from them. Reproducing the half-half as a synchronization
3765 // would make the sample path decide hit against miss by a coin
3766 // instead of by reading the cache, which is what it did.
3767 bool cache_read_class = false;
3768 if (nd.nodetype == NodeType::Cache) {
3769 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci =
3770 sn.nodeparam.find(ind);
3771 if (ci != sn.nodeparam.end()) {
3772 if (r - 1 < ci->second.pread.size() && !ci->second.pread[r - 1].empty()) {
3773 Sync<T> s;
3774 s.active = SyncEvent<T>{EventType::READ, ind, r, one};
3775 s.passive = SyncEvent<T>{EventType::READ, local, r, one};
3776 sync.push_back(s);
3777 }
3778 cache_read_class =
3779 r - 1 < ci->second.hitclass.size() && ci->second.hitclass[r - 1] != 0;
3780 }
3781 }
3782 // A stateful Transition emits one server phase-change action per
3783 // MODE, not per class (the JAR gates the same way on the first
3784 // class); its cls slot carries the mode, as `after_event_transition`
3785 // reads it.
3786 if (nd.nodetype == NodeType::Transition && r == 1) {
3787 const typename std::map<std::size_t, TransitionParam<T>>::const_iterator ti =
3788 sn.transparam.find(ind);
3789 if (ti != sn.transparam.end()) {
3790 for (std::size_t m = 1; m <= ti->second.nmodes; ++m) {
3791 Sync<T> s;
3792 s.active = SyncEvent<T>{EventType::PHASE, ind, m, one};
3793 s.passive = SyncEvent<T>{EventType::LOCAL, local, m, one};
3794 sync.push_back(s);
3795 }
3796 }
3797 }
3798 if (cache_read_class) continue;
3799
3800 const std::size_t isf = sn.stateful_index(ind);
3801 for (std::size_t jnd = 1; jnd <= sn.nodes.size(); ++jnd) {
3802 if (!sn.nodes[jnd - 1].stateful) continue;
3803 const std::size_t jsf = sn.stateful_index(jnd);
3804 for (std::size_t s = 1; s <= R; ++s) {
3805 const T p = sn.rt((isf - 1) * R + (r - 1), (jsf - 1) * R + (s - 1));
3806 if (num_traits<T>::to_double(p) <= 0) continue;
3807 Sync<T> ns;
3808 ns.active = SyncEvent<T>{EventType::DEP, ind, r, one};
3809 ns.passive = SyncEvent<T>{EventType::ARV, jnd, s, p};
3810 // SDR reaches `rt` as the uniform placeholder `refresh_routing`
3811 // writes, so the SUPPORT of the mask is right and the values
3812 // are not: the pair exists exactly where a link does, and the
3813 // generator replaces the probability state by state. This is
3814 // the reference's `rtmask = rtfun(emptystate, emptystate)`,
3815 // which likewise keeps every connected pair.
3816 if (nd.routing.size() >= s && nd.routing[s - 1] == RoutingStrategy::SDR) {
3817 ns.passive.statedep = true;
3818 ns.passive.rt_row = (isf - 1) * R + (r - 1);
3819 ns.passive.rt_col = (jsf - 1) * R + (s - 1);
3820 }
3821 sync.push_back(ns);
3822 }
3823 }
3824 }
3825 }
3826 return sync;
3827}
3828
3829/**
3830 * Port of `State.afterFJEvent`: fire ONE entry of the fork firing list.
3831 *
3832 * A fork firing is atomic across several nodes -- it consumes the parent at the
3833 * fork and places one sibling at each branch head in the same instant -- so
3834 * unlike every ordinary transition it cannot be decomposed into an active half
3835 * and a passive half. It therefore takes the whole network state, exactly as an
3836 * SPN global synchronization does.
3837 *
3838 * THE TWO ENABLING CONDITIONS.
3839 *
3840 * 1. The fork holds at least one class-r parent.
3841 *
3842 * 2. This entry's tag is the LOWEST FREE tag for this (fork, class). A tag is
3843 * free when its auxiliary classes have zero occupancy NETWORK-WIDE, which is
3844 * why the test scans every stateful node and not just the branches: a
3845 * sibling in transit is still outstanding. Without the canonical choice
3846 * every firing would produce one successor per free tag, all of them
3847 * relabellings of each other, and the chain would carry a factorial number
3848 * of duplicate states.
3849 *
3850 * The emission is applied SEQUENTIALLY over a growing outcome list rather than
3851 * branch-by-branch into one state, because that is what handles the three cases
3852 * a single pass would get wrong: two branches sharing a head node, `weight > 1`
3853 * repeated emissions on one branch, and a non-exponential sibling service whose
3854 * phase-entry mixture makes one arrival into several outcomes.
3855 */
3856template <class T>
3858 const NetState<T>& gl) {
3859 const std::size_t R = sn.nclasses;
3860 GlobalOutcome<T> out;
3861 const std::size_t isf_f = sn.stateful_index(e.fork);
3862 if (isf_f == 0) return out;
3863 const std::vector<T>& fs = gl.local[isf_f - 1];
3864 if (fs.size() < R) return out;
3865 if (num_traits<T>::to_double(fs[fs.size() - R + e.cls - 1]) < 1) return out;
3866
3867 // Tag occupancy, network-wide.
3868 const std::size_t B = e.auxall.size();
3869 if (B == 0 || e.tag == 0 || e.tag > e.auxall[0].size()) return out;
3870 const std::size_t Tt = e.auxall[0].size();
3871 std::vector<double> nglobal(R, 0.0);
3872 for (std::size_t isf = 1; isf <= sn.stateful_nodes.size(); ++isf) {
3873 const std::pair<T, std::vector<T>> mg =
3874 to_marginal_aggr(sn, sn.stateful_nodes[isf - 1], gl.local[isf - 1]);
3875 for (std::size_t r = 0; r < R; ++r) {
3876 const double v = num_traits<T>::to_double(mg.second[r]);
3877 // A Source encodes its reservoir as an infinite marginal; adding it
3878 // would make every tag look occupied. An auxiliary class is never
3879 // generated by a Source, so skipping the non-finite entries cannot
3880 // hide a real sibling.
3881 if (std::isfinite(v)) nglobal[r] += v;
3882 }
3883 }
3884 std::vector<double> occ(Tt, 0.0);
3885 for (std::size_t t = 0; t < Tt; ++t)
3886 for (std::size_t b = 0; b < B; ++b) occ[t] += nglobal[e.auxall[b][t] - 1];
3887 if (occ[e.tag - 1] > 0) return out; // this tag is in use
3888 for (std::size_t t = 0; t + 1 < e.tag; ++t)
3889 if (occ[t] == 0) return out; // a lower tag is free
3890
3891 NetState<T> seed = gl;
3892 seed.local[isf_f - 1][seed.local[isf_f - 1].size() - R + e.cls - 1] -=
3894
3895 std::vector<NetState<T>> partials(1, seed);
3896 std::vector<T> partprob(1, num_traits<T>::from_int(1));
3897 // The emission list, one entry per sibling. `weightlink` is filled only when
3898 // the fork sends different counts down different links; the interleave below
3899 // reproduces `repmat(1:B,1,w)` exactly in the uniform case, so a plain fork
3900 // walks the order it always did.
3901 std::vector<std::size_t> emissions;
3902 if (e.weightlink.empty()) {
3903 const std::size_t w = e.weight == 0 ? 1 : e.weight;
3904 for (std::size_t rep = 0; rep < w; ++rep)
3905 for (std::size_t b = 0; b < B; ++b) emissions.push_back(b);
3906 } else {
3907 std::size_t wmax = 0;
3908 for (std::size_t b = 0; b < e.weightlink.size(); ++b)
3909 if (e.weightlink[b] > wmax) wmax = e.weightlink[b];
3910 for (std::size_t rep = 1; rep <= wmax; ++rep)
3911 for (std::size_t b = 0; b < B; ++b)
3912 if (b < e.weightlink.size() && e.weightlink[b] >= rep) emissions.push_back(b);
3913 }
3914 for (std::size_t ei = 0; ei < emissions.size(); ++ei) {
3915 const std::size_t b = emissions[ei];
3916 const std::size_t bh = e.branchheads[b];
3917 const std::size_t isf_b = sn.stateful_index(bh);
3918 if (isf_b == 0) return GlobalOutcome<T>();
3919 const std::size_t a = e.auxclasses[b];
3920 std::vector<NetState<T>> nextp;
3921 std::vector<T> nextq;
3922 for (std::size_t pp = 0; pp < partials.size(); ++pp) {
3923 const EventOutcome<T> arv =
3924 after_event(sn, bh, partials[pp].local[isf_b - 1], EventType::ARV, a);
3925 if (arv.space.empty()) return GlobalOutcome<T>(); // blocked: no firing
3926 for (std::size_t io = 0; io < arv.space.size(); ++io) {
3927 NetState<T> ns = partials[pp];
3928 ns.local[isf_b - 1] = arv.space[io];
3929 nextp.push_back(ns);
3930 nextq.push_back(io < arv.prob.size() ? T(partprob[pp] * arv.prob[io])
3931 : partprob[pp]);
3932 }
3933 }
3934 partials.swap(nextp);
3935 partprob.swap(nextq);
3936 }
3937
3938 out.space = partials;
3939 for (std::size_t i = 0; i < partials.size(); ++i) {
3941 out.prob.push_back(T(partprob[i] * e.prob));
3942 out.completion.push_back(true);
3943 }
3944 return out;
3945}
3946
3947} // namespace qn
3948} // namespace line
3949
3950#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:424
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
@ 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:373
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:372
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:371
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:374
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:639
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:385
@ FIFO
first in, first out
Definition lang_types.h:380
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.
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.
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:759
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
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
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:674
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:679
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:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141
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