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