lang.+State

State.afterEventRouter(sn, ind, event, class, isSimulation, eventCache, space_buf, space_srv, space_var, key)

[OUTSPACE, OUTRATE, OUTPROB, EVENTCACHE] = AFTEREVENTROUTER(SN, IND, EVENT, CLASS, ISSIMULATION, EVENTCACHE, SPACE_BUF, SPACE_SRV, SPACE_VAR, KEY)

Handle router afterEvent logic

State.afterEventFork(sn, ind, event, class, isSimulation, eventCache, space_buf, space_srv, space_var, key)

#ok<INUSL> [OUTSPACE, OUTRATE, OUTPROB, EVENTCACHE] = AFTEREVENTFORK(SN, IND, EVENT, CLASS, ISSIMULATION, EVENTCACHE, SPACE_BUF, SPACE_SRV, SPACE_VAR, KEY)

Handle afterEvent logic for stateful Fork nodes (FJ-augmented structs only, see ModelAdapter.fjtag). The fork state is a per-class count of parent jobs momentarily held before the fork firing. Arrivals are buffered here; the atomic multi-branch emission is not a DEP event but a fork firing synchronization (sn.fjsync) handled by State.afterFJEvent.

State.replyBlockInfo(sn, ind)

RINFO = REPLYBLOCKINFO(SN, IND)

Layout of the synchronous-call (REPLY signal) blocked-server block that node IND carries in its local-variable state, as a struct with fields

classes : calling classes that can hold a server at this node off : offset of the block inside the node’s local-variable vector slot : 1 x nclasses index into the local-variable vector, 0 when the

class holds no slot here

width : number of columns in the block

A job of a class R with SN.SYNCREPLY(R) >= 0 makes a synchronous call: it leaves this station for the callee but KEEPS its server, which stays held until the matching REPLY signal class comes back here. The held servers are not derivable from the marginal state (the job is at the callee, not here), so they are counted per class in this block. LDES keys the same information by job id (Solver_ssj.pendingReplyMap); a CTMC has no job identity, so it carries counts.

The block trails the modulation, routing and node blocks of SN.NVARS (columns 1..R, R+1..2R and 2R+1), occupying columns 2R+1+r. Appending keeps every existing nvars reader valid, and the columns stay zero-width for models without reply signals, so no other model changes state width.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.pollingInfo(sn, ind)

PINFO = POLLINGINFO(SN, IND)

Derived description of the polling controller at the station of node IND. Returns [] when the node is not a polling station.

The polling controller is stored in the trailing local-variable block of the station state, whose width is sn.nvars(ind,2*R+1). The block holds, in order, the columns [pos, swk, ctr]; each is materialized only when the discipline actually needs it (see the widths below), so that a polling station never carries state that its dynamics cannot distinguish.

pos index of the buffer the server is currently at (serving) or heading to

(switching). It is materialized only when at least one switchover is non-immediate: while a job is in service pos always equals the class of that job, and while the station is empty and every switchover is immediate the server position is unobservable (see PARKED below).

swk 0 when the server sits at pos, otherwise the phase of the switchover

PH into buffer pos. Materialized only when some switchover is non-immediate.

ctr the visit budget, materialized for every discipline except EXHAUSTIVE:
GATED jobs of class pos admitted at the polling instant that

have not completed yet (the job in service counts as one of them), so the visit ends when ctr reaches 0;

KLIMITED services still permitted in this visit, the one in

progress included;

DECREMENTING the target class-pos population: the visit ends once

the population has dropped to ctr, i.e. one below the level found at the polling instant (semi-exhaustive).

The three tangible controller configurations are therefore

SERVING(p) swk=0, one class-p job in the service facility; SWITCHING(p) swk>0, service facility empty; PARKED swk=0, service facility empty, station empty. Reachable only

when every switchover is immediate, in which case a server that completes a full lap without finding work would otherwise cycle in zero time forever. pos is then unobservable and canonical.

Immediate() switchovers are NOT represented as states: Immediate.getProcess returns an exponential of rate GlobalConstants.Immediate, and taking that literally would put a ~1e8 rate in the generator (stiff, and a spurious state per buffer). They are instead folded into the enclosing transition by State.pollingNext, which walks the cyclic order until a tangible state.

State.fromMarginalAndStarted(sn, ind, n, s, options)

Wrapper: the discipline branches below return early from several places, so the synchronous-call (REPLY) counter columns are appended here, once, for every exit path. Without them the initial state is narrower than the enumerated local space, matchrow fails, and solver_ctmc silently skips its unreachable-state pruning – leaving the enumerated-but-unreachable “counter set while every job is here” states as a second absorbing class.

State.fromMarginalAndRunning(sn, ind, n, s, options)

SPACE = FROMMARGINALANDRUNNING(QN, IND, N, S, OPTIONS)

State.reachabilityAnalysis(sn, cutoff, options)

[SS] = RECHABILITYANALSYSIS(QN, CUTOFF)

State.fromMarg(sn, ind, ntot, options)

FROMMARG Generate the state space with a given TOTAL queue length

@brief Creates the state space where a node holds NTOT jobs in total @param sn Network structure or Network object @param ind Node index @param ntot Total number of jobs at the node, all classes summed @param options Optional structure with configuration parameters @return space Generated state space with the requested total

This is the class-summed counterpart of State.fromMarginal: where fromMarginal fixes how many jobs of EACH class the node holds, fromMarg fixes only how many jobs it holds ALTOGETHER, and returns the union of fromMarginal over every class split of NTOT the node can hold.

A class that is disabled at the station has classcap 0 and is excluded from the split enumeration up front rather than after the fact. Asking fromMarginal for a job of such a class yields an EMPTY local space, and an empty factor is absorbed by the cartesian product instead of annihilating it, so the job would silently disappear; see the same trap documented at cpp/include/line/lang/qn/state.h.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.pollingSpace(sn, ind, space)

SPACE = POLLINGSPACE(SN, IND, SPACE)

Append the polling controller columns to the rows of SPACE, which must hold the [buffer, server, routing-variable] layout of a polling station. Rows are expanded into one row per controller configuration the discipline can occupy, and rows for which no configuration exists are dropped.

Enumerating the controller per row rather than as a blind cartesian product is what keeps the state space tight and the chain irreducible: pos is pinned to the class in service, a switchover excludes a busy service facility, and a park excludes a non-empty station. A cartesian product would instead admit states such as “serving buffer 1 while a class-2 job occupies the server”, which no transition can reach or leave in a way consistent with the marginals.

State.pollingNext(pinfo, pos, nbuf, R, arrived)

[Q, MODE, BUDGET] = POLLINGNEXT(PINFO, POS, NBUF, R, ARRIVED)

Resolve the tangible controller state a polling server reaches once it stops serving buffer POS. NBUF(r) is the number of class-r jobs waiting in the buffer (the service facility is empty at this point).

ARRIVED (default false) selects where the cyclic walk starts. When false the server is LEAVING pos, so the walk starts at pos+1; when true the server has just ARRIVED at pos (a switchover into pos has completed, or the server is parked at pos) and pos itself is examined first, without charging its switchover a second time.

MODE = 1 start a visit to buffer q: q has work and is reached in zero time.

BUDGET is the initial value of the ctr column for that visit.

MODE = 2 enter the switchover into buffer q: a strictly positive timer, so

this is where the server dwells. BUDGET is 0 (the visit budget is only set once the server arrives at q).

MODE = 0 park at q: the walk completed a full lap without finding work and

without meeting a timed switchover, which can only happen when the station is empty and every switchover is immediate. The server would otherwise cycle in zero time forever, so it is held here until the next arrival. q is canonical and unobservable.

The walk is what makes an Immediate() switchover a zero-time leg rather than a state: the server passes straight through such a buffer when it has no work, and only stops at a buffer that either has work or costs time to reach. This is the classical cyclic-polling discipline, in which the server visits the buffers in strict cyclic order and pays the switchover of every buffer it moves to, whether or not that buffer turns out to have work.

State.pollingBlocks(pinfo, srvclass, nbuf, R)

TRIPS = POLLINGBLOCKS(PINFO, SRVCLASS, NBUF, R)

Every controller configuration compatible with a service facility holding a class-SRVCLASS job (0 when empty) and buffers holding NBUF. Returns one [pos, swk, ctr] triple per configuration, in full regardless of which columns State.pollingInfo materializes (project with State.pollingProject), and zero rows when the combination is unoccupiable.

The three tangible configurations of State.pollingInfo map as follows.

SERVING(p): the server stands at the buffer of the job it is serving, so pos is pinned to srvclass. The visit budget is free within the bounds its discipline can have left it in:

GATED ctr counts the jobs admitted at the polling instant that have

not completed, the one in service included, so ctr >= 1; and the ctr-1 still uncompleted ones are all waiting in the buffer, so ctr-1 <= nbuf(p).

KLIMITED 1 <= ctr <= K, the one in service counted. DECREMENTING ctr is the population the visit is driving the class down to;

it started at one below the level found and the visit is still running, so the current population nbuf(p)+1 exceeds it.

SWITCHING(q): the facility must be empty, and q must be a buffer whose switchover takes time, since a zero-time one is never dwelt in. Every phase of that switchover is reachable. Jobs may wait meanwhile: this is exactly the configuration that makes a polling station non-work-conserving.

PARKED: the facility and the whole station are empty and no switchover takes time. With work waiting and only immediate switchovers the server would have reached it in zero time, so an idle facility and a non-empty station is unoccupiable and yields no rows at all. That pruning matters even when no column is materialized: leaving those rows in the state space would make the generator reducible.

State.isCatastropheSignal(sn, class)

BOOL = ISCATASTROPHESIGNAL(SN, CLASS)

True if signal class CLASS empties a station on arrival. The signaltype is consulted alongside the iscatastrophe flag so that the two encodings of a catastrophe cannot disagree (SolverMAM applies the same test).

State.isValid(sn, n, s, options)

ISVALID Validate network state against capacity and scheduling constraints

@brief Checks if a given state is valid according to network constraints @param sn Network structure or Network object @param n Number of jobs per class at the station @param s Number of jobs per class that are currently running/being served @param options Optional validation configuration parameters @return isValid Boolean indicating if the state satisfies all constraints

This function validates whether a proposed network state is feasible given the network’s capacity limitations, scheduling constraints, and other structural requirements. It is essential for state space generation and state-based analysis methods.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.tagArc(tags, ntot, nblk, R, cls)

TAGS = TAGARC(TAGS, NTOT, NBLK, R, CLS)

Record a START or PREEMPT tag on the block of NBLK successor rows that ends at row NTOT of outspace, i.e. on the rows just appended by the caller. CLS is the class the tag refers to: a scalar shared by the whole block, or one entry per row of the block, with 0 meaning “no tag on this row”.

Rows appended earlier and left untagged are padded with zeros here rather than at their own append site, so only the arcs that actually carry a tag need to say anything. The caller pads the tail once, after the last append, with State.tagPad.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.afterEventStationSignal(sn, ind, ist, inspace, class, K, Ks, S, pie, space_buf, space_srv, space_var)

[OUTSPACE, OUTRATE, OUTPROB, OUTSTART] = AFTEREVENTSTATIONSIGNAL(SN, IND, IST, INSPACE, CLASS, K, KS, S, PIE, SPACE_BUF, SPACE_SRV, SPACE_VAR)

OUTSTART is the START annotation of each successor row (see State.tagArc): removing an IN-SERVICE victim hands its server to the head of the waiting line, which is a service start like a departure promotion. Removing a WAITING victim starts nobody. A batch removal can therefore start several jobs on one arc, and because two removal orders may end in the same destination state with different promotions, the merged row carries the probability-weighted EXPECTED number of starts – which is what the START filtration integrates against the arc weight.

Passive arrival of a G-network signal class at station IST. A signal never joins the station: it acts on the jobs already there and is annihilated.

CATASTROPHE empties the station of every job, ignoring the removal count distribution (a catastrophe removes all jobs by definition).

NEGATIVE removes a batch of jobs:
  • Victims. When the signal declares a target class (setmodel via forJobClass, sn.signaltarget >= 1) only that class is eligible; otherwise every non-signal class is eligible. The untargeted case is the classic Gelenbe negative customer and agrees with SolverMAM and SolverLDES, which are both class-agnostic.

  • Count. sn.signalremdist gives the batch-size pmf. An oversized batch empties the eligible jobs rather than driving the queue negative, so the pmf tail P(B >= n) lumps onto “remove all n” (the min(B,n) clipping used by LDES and the tail term used by MAM).

  • Policy. sn.signalrempolicy selects the victim: FCFS removes the oldest waiting job, LCFS the newest, RANDOM draws uniformly over waiting and in-service jobs. FCFS/LCFS only touch an in-service job when no job is waiting (two-tier, as in LDES).

The station state carries no arrival-time order for in-service jobs, so when a policy has to reach into the servers the victim is drawn uniformly across the occupied phases (exact whenever at most one job of the class is in service, which covers every single-server station).

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.spaceCacheCompositions(total, parts)

C = SPACECACHECOMPOSITIONS(TOTAL, PARTS)

Enumerate the weak compositions of TOTAL into exactly PARTS non-negative integers, one composition per row.

State.cacheRetrievalClassMap(sn, ind)

[RCLIST, RCITEMS, RCORIGCLASS] = CACHERETRIEVALCLASSMAP(SN, IND)

Canonical ordering of the retrieval classes of cache node IND, used to index block B of the cache local-variable vector (the per-retrieval-class counts of merged secondary requests).

rcList retrieval class indices, ascending rcItems item served by each entry of rcList rcOrigClass originating (arrival) class of each entry of rcList

Returns empty arrays when the node has no retrieval system.

State.afterEventCache(sn, ind, event, class, isSimulation, eventCache, R, space_buf, space_srv, space_var, key)

job arrives in class, then reads and moves into hit or miss class, then departs

State.afterEventTransition(sn, ind, inspace, K, Ks, event, class, isSimulation, eventCache, R, space_buf, space_srv, space_fired, space_var, key)

job arrives in class, then reads and moves into hit or miss class, then departs

State.getHashOrAdd(sn, ind, inspace)

[HASHID, QN] = GETHASHORADD(QN, IND, INSPACE)

State.getHash(sn, ind, inspace)

HASHID = GETHASH(QN, IND, INSPACE)

State.spaceGenerator(sn, cutoff, options)

SPACEGENERATOR Generate complete state space for queueing network analysis

@brief Creates the complete state space including all possible network states @param sn Network structure representing the queueing network @param cutoff Population cutoff limits for open classes (scalar or matrix) @param options Optional configuration structure for state generation @return SS Complete state space matrix @return SSh Hashed state space for efficient lookups @return sn Updated network structure with state space information @return Adj Adjacency matrix for state transitions (SPN support) @return ST State transition information (SPN support)

The state space generator creates all possible states for the queueing network, including those not reachable from the initial state. This is essential for steady-state analysis and performance metric computation. For open classes, a cutoff parameter limits the maximum population.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.toMarginal(sn, ind, state_i, phasesz, phaseshift, space_buf, space_srv, space_var)

#ok<INUSD> TOMARGINAL Compute marginal distributions for a specific node

[NI, NIR, SIR, KIR] = TOMARGINAL(SN, IND, STATE_I, PHASESZ, PHASESHIFT, SPACE_BUF, SPACE_SRV, SPACE_VAR)

@brief Extracts marginal job statistics from global state information for a specific node

This function processes the global network state to compute marginal distributions and job counts for a specific node, considering different scheduling strategies, service phases, and node types.

@param sn Network structure or Network object @param ind Node index to extract marginal information for @param state_i Global state matrix or vector @param phasesz Vector of phase sizes for each class @param phaseshift Phase shift parameters for state extraction @param space_buf Buffer space configuration @param space_srv Service space configuration @param space_var Variable space configuration

@return ni Total jobs in node IND @return nir Total jobs per class in node IND [matrix: states x classes] @return sir Total jobs in service per class in node IND [matrix: states x classes] @return kir Total jobs in service per class and per phase in node IND [3D array: states x classes x phases]

State.cartesian(inspace1, inspace2)

INSPACE1 = CARTESIAN(INSPACE1, INSPACE2)

Cartesian product of two matrices. It replicates elements of the first input matrix and pairs them with each row of the second input matrix.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.spaceClosedMultiCS(M, N, chains, caps)

SS = SPACECLOSEDMULTICS(M, N, CHAINS) SS = SPACECLOSEDMULTICS(M, N, CHAINS, CAPS)

CAPS, when given, is an MxK per (node, class) capacity handed through to spaceClosedMulti so the per-class distribution is pruned at the branch.

State.afterEventHashedOrAdd(sn, ind, inhash, event, class)

[OUTHASH, OUTRATE, OUTPROB, QN] = AFTEREVENTHASHEDORADD(QN, IND, INHASH, EVENT, CLASS)

State.toMarginalAggr(sn, ind, state_i, K, Ks, space_buf, space_srv, space_var)

#ok<INUSD> TOMARGINALAGGR Compute aggregate marginal distributions for a specific node

[NI, NIR] = TOMARGINALAGGR(SN, IND, STATE_I, K, KS, SPACE_BUF, SPACE_SRV, SPACE_VAR)

@brief Computes aggregate marginal job counts from state information without phase details

This function provides a simplified version of toMarginal that computes aggregate job counts per node and per class, without considering individual service phases. It is more efficient when phase-level detail is not required.

@param sn Network structure or Network object @param ind Node index to extract marginal information for @param state_i Global state matrix or vector @param K Vector of population for each class @param Ks Matrix of populations per chain and class @param space_buf Buffer space configuration @param space_srv Service space configuration @param space_var Variable space configuration

@return ni Total jobs in node IND (aggregate across all classes) @return nir Jobs per class in node IND [vector: classes]

State.afterEventStationReply(sn, ind, ist, class, K, Ks, S, pie, space_buf, space_srv, space_var)

[OUTSPACE, OUTRATE, OUTPROB, OUTSTART] = AFTEREVENTSTATIONREPLY(SN, IND, IST, CLASS, K, KS, S, PIE, SPACE_BUF, SPACE_SRV, SPACE_VAR)

OUTSTART is the START annotation of each successor row (see State.tagArc): the reply takes the server it just released whenever one is free, which is a service start like any other; a reply that has to queue starts nobody.

Arrival of a REPLY signal class at the FCFS station that is holding a server for the matching synchronous call. The reply completes the call:

  1. one held server of the calling class is released (the reply block counter is decremented);

  2. if jobs are waiting, the head of line takes the released server, since those jobs queued before the reply arrived (FCFS buffer is right-aligned, head of line = rightmost occupied slot);

  3. the reply itself then joins like an ordinary arrival: into a server if one is still free, otherwise at the tail of the buffer.

Unlike a NEGATIVE or CATASTROPHE signal the reply is NOT annihilated: it is a job that carries the call result onward, so it is served here (typically Immediate) and routed on by the ordinary routing matrix. This mirrors LDES, where a REPLY arrival frees the blocked server and then continues routing.

The event is passive: the rate is set by the active departure at the callee.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.spaceLocalVars(sn, ind, maxPending)

SPACE = SPACELOCALVARS(QN, IND, MAXPENDING)

MAXPENDING (optional, default 0) bounds the number of secondary (delayed-hit) requests that a cache node may merge onto a single in-flight fetch.

State.afterEventJoin(sn, ind, inspace, event, class, isSimulation, eventCache, key)

[OUTSPACE, OUTRATE, OUTPROB, EVENTCACHE] = AFTEREVENTJOIN(SN, IND, INSPACE, EVENT, CLASS, ISSIMULATION, EVENTCACHE, KEY)

Handle afterEvent logic for Join stations on FJ-augmented structs (see ModelAdapter.fjtag). The join state is a plain per-class count vector [n_1..n_R] of buffered jobs: auxiliary-class entries count the sibling tasks waiting for synchronization.

ARV (passive): buffer the arriving job/sibling. DEP in an original class r (active, immediate): enabled when a plain class-r job is buffered, or when some tag t has the full required sibling multiset present (all B branches, identity matching by tag). The firing consumes the siblings of the LOWEST complete tag and lets one class-r job depart; the r->destination move is the regular sync pairing generated by refreshSync from the join routing.

State.replyBlocked(sn, ind, space_var)

[B, NB] = REPLYBLOCKED(SN, IND, SPACE_VAR)

Servers held at node IND by jobs that made a synchronous call and are waiting for their REPLY signal. B is (rows x nclasses) per-class counts, one row per row of SPACE_VAR, and NB is the (rows x 1) total. Returns zeros when the node carries no synchronous-call block, so callers can subtract NB from the server count unconditionally.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.reachableSpaceGenerator(sn, options)

[SSQ,SSH,QN] = REACHABLESPACEGENERATOR(QN,OPTIONS)

This differs from spaceGenerator as it is restricted to states reachable from the initial state.

State.isPhysicalCapacity(sn, ist, class)

TF = ISPHYSICALCAPACITY(SN, IST, CLASS)

True when the capacity bound at station IST for CLASS is a PHYSICAL finite capacity (setCapacity/setClassCapacity), as opposed to a state-space CUTOFF imposed on an open class only to bound enumeration.

This distinction matters because the producer’s capacity/classcap arguments have the open-class cutoff folded in: solver_ssa overwrites sn.cap/sn.classcap with min(cutoff, physical), so at the cutoff boundary they are finite even when there is no physical cap. Treating a cutoff boundary as a physical one would turn a state-space truncation into a self-loop loss (wrong ArvR and a perturbed sample path). Only a physical cap should trigger the loss/block refusal semantics; a cutoff-only refusal must fall back to the pre-change truncation (place the job, let the en_o capacity filter delete the row).

The reliable in-producer signal is the DROP RULE. refreshCapacity sets a finite-capacity drop rule (DROP, or a blocking/retrial rule) exactly when the station has a physical finite capacity for the class; an open class bounded only by the cutoff keeps the WAITQ default. So a non-WAITQ, non-unset drop rule marks a physical capacity. (A user who explicitly sets WAITQ on a physical cap is the one ambiguous case; it is already ill-defined – CTMC drops, JMT blocks – and is treated here as a cutoff, i.e. truncated.)

State.arrivalIsLost(sn, ist, class)

TF = ARRIVALISLOST(SN, IST, CLASS)

True when an arrival of CLASS that finds no room at station IST is LOST, and false when it must BLOCK the upstream instead. This is the single predicate that decides the refusal semantics; every refusal path must branch on it.

The rule is the CLASS TYPE, not the drop rule:

OPEN class -> LOST. The external arrival stream is memoryless, so a job

that finds the station full simply never enters. The caller must then leave the state UNCHANGED (a self-loop): the arrival event still fires, so the offered rate reaches the arrival-rate statistic and the loss shows up as ArvR - Tput. A self-loop cancels on the generator diagonal and therefore cannot perturb the stationary distribution, so QLen/Util/Tput are unaffected.

CLOSED class -> BLOCKED. A closed network’s N jobs have nowhere to go;

population conservation is a defining invariant, so a closed job can never be dropped. The caller must return an EMPTY outspace, which disables the upstream departure until room frees.

An explicit blocking drop rule (BAS/BBS/RSRD) also asks for blocking, for any class: the user has said the job must wait rather than be lost.

This is the same open/closed predicate as the CTMC analyzer’s canDropClass and the BUG-12 utilization guard. Note the conventions are complementary, not contradictory: the arrival rate counts the OFFERED job (this predicate lets the event fire), while Util/QLen/Tput count only the CARRIED one.

State.afterEventInit(sn)

CTX = AFTEREVENTINIT(SN) Precomputes the loop-invariant setup of State.afterEvent so that hot callers (e.g., the solver_ssa Gillespie loop, which re-evaluates every synchronization at every step) avoid re-deriving it on each call.

IMPORTANT: ctx caches sn.nservers, sn.cap and sn.classcap. It must be built AFTER any caller-side rewrite of these fields (solver_ssa rewrites them in its preamble: Inf servers at delay nodes and open-class cutoffs) and AFTER sn_nonmarkov_toph, and the SAME sn must be passed to afterEvent alongside ctx. Building ctx from a stale sn silently drops blocking and server-count semantics.

State.fromMarginalBounds(sn, ind, lb, ub, cap, options)

SPACE = FROMMARGINALBOUNDS(QN, IND, LB, UB, CAP, OPTIONS)

State.signalBatchPMF(sn, class, ntot)

[KVALS, KPROBS] = SIGNALBATCHPMF(SN, CLASS, NTOT)

Batch-size distribution of the jobs removed by signal class CLASS when NTOT eligible jobs are present. Without a removal distribution a signal removes exactly one job. With one, the pmf is clipped at NTOT: a batch larger than the eligible population empties it instead of driving the queue negative, so the tail P(B >= NTOT) lumps onto “remove NTOT”. This matches the min(B, n) clipping in SolverLDES and the tail term that SolverMAM puts on the empty state.

State.pollingSet(pinfo, space_var, pos, swk, ctr)

SPACE_VAR = POLLINGSET(PINFO, SPACE_VAR, POS, SWK, CTR)

Write the polling controller into the local-variable columns of a state row. Columns that State.pollingInfo elides are dropped: they are reconstructible from the rest of the state (see State.pollingGet), so materializing them would split each state into copies that no observation can tell apart.

State.pollingProject(pinfo, trips)

BLOCK = POLLINGPROJECT(PINFO, TRIPS)

Project full [pos, swk, ctr] controller triples onto the columns that State.pollingInfo materializes. The elided columns are reconstructible from the rest of the state (see State.pollingGet), so keeping them would split each state into copies no observation can tell apart.

State.pollingLand(pinfo, q, mode, budget, space_buf, space_srv, space_var, K, Ks, pieist, R)

[ROWS, PROBS] = POLLINGLAND(PINFO, Q, MODE, BUDGET, SPACE_BUF, SPACE_SRV, SPACE_VAR, K, KS, PIEIST, R)

Materialize the state rows a polling server lands in after State.pollingNext has resolved (Q, MODE, BUDGET), together with the probability of each. The three inputs SPACE_BUF/SPACE_SRV/SPACE_VAR are single rows describing the station at the instant the decision is taken, i.e. with the completed job (if any) already removed from the service facility.

PROBS splits a landing across the entry phases of a phase-type: which phase a service or a switchover starts in is a random choice, so one decision yields one row per entry phase, weighted by the corresponding entry probability. Callers fold PROBS into the transition rate rather than into outprob, since the branching happens at the instant the active event fires.

State.pollingInit(sn, ind, nbuf, srvclass)

BLOCK = POLLINGINIT(SN, IND, NBUF, SRVCLASS)

Canonical initial value of the polling controller columns for a station whose buffers hold NBUF and whose service facility holds a class-SRVCLASS job (0 when empty). Returns a 1-by-pinfo.width row, empty when the node is not a polling station or carries no materialized controller column.

The row is always one that State.pollingBlocks also enumerates, so that the initial state is a member of the generated state space.

State.pollingGet(pinfo, space_var, srvclass)

[POS, SWK, CTR] = POLLINGGET(PINFO, SPACE_VAR, SRVCLASS)

Read the polling controller out of the local-variable columns of a single state row. SRVCLASS is the class currently in the service facility, or 0 when the facility is empty.

The columns that State.pollingInfo elides are reconstructed here, so that callers always see a complete controller:

pos when not materialized, every switchover is immediate, so the server is

either serving (and then it stands at the buffer of the job in service) or parked (and then its position is unobservable: from a park with only immediate switchovers the walk reaches any buffer in zero time, so all positions have identical dynamics and 1 is canonical).

swk when not materialized, no switchover takes time, so the server is

never inside one.

ctr when not materialized the discipline is EXHAUSTIVE, which bounds a

visit by the buffer draining rather than by a budget.

State.pollingBudget(pinfo, nbufq)

BUDGET = POLLINGBUDGET(PINFO, NBUFQ)

Initial value of the ctr column of a visit that starts at a buffer holding NBUFQ waiting jobs. NBUFQ is the class-q population at the polling instant: the service facility is empty when a visit starts, so it is the whole class-q population at the station.

See State.pollingInfo for the meaning of ctr under each discipline.

State.cdclassfactor(beta, nirmat, rows, class)

F = CDCLASSFACTOR(BETA, NIRMAT, ROWS, CLASS)

Per-row class-dependence factor for event rates. BETA is a class-dependence handle mapping a 1xR per-class population vector n to the 1xR vector of dimensionless rate scalings beta_r(n) (see fes_beta_handle). NIRMAT is the per-state per-class population matrix (nstates x R, one row per enabled state). ROWS selects the enabled rows (logical mask or index vector over the rows of NIRMAT) and CLASS is the class completing service.

For each selected row j the handle is evaluated on that row’s population vector and the component of the completing class is returned:

F(j) = v(min(CLASS, numel(v))), v = BETA(NIRMAT(ROWS(j),:))

so a neutral scaling @(n) 1 (scalar) yields F(j) = 1 for every class.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.spaceClosedMulti(M, N, caps)

SS = SPACECLOSEDMULTI(M, N) SS = SPACECLOSEDMULTI(M, N, CAPS)

CAPS, when given, is an MxR per (node, class) bound. It is handed straight through to spaceClosedSingle, which prunes the per-class enumeration at the branch: a (node, class) pair the class never visits has capacity 0 and contributes only the zero slot.

State.initialOccupancy(sn, ind, r)

N = INITIALOCCUPANCY(SN, IND, R)

Number of class-R jobs held by node IND in the DECLARED initial state, or 0 when no initial state is available.

Used to bound the enumerated local state space from below. A station whose visit ratio is zero is never re-entered, but it may still hold jobs at time zero: an SPN place with no input arc is a TRANSIENT state of the chain, not an absent one. Enumerating up to the initial occupancy keeps that state in the space; the unreachable-state pruning in solver_ctmc then removes whatever the chain cannot actually reach.

See _kb/11-conventions-and-gotchas.md.

State.spaceCachePendings(s, maxPending)

P = SPACECACHEPENDINGS(S, MAXPENDING)

Enumerate the ways in which up to MAXPENDING secondary (delayed-hit) requests can be distributed over S concurrent in-flight fetches. Returns one row per assignment, each row holding S non-negative counts summing to at most MAXPENDING. For S = 0 the single empty assignment is returned.

State.spaceCache(n, m, retrievalSystemCapacity, maxPending, retrievalClassItems)

SS = SPACECACHE(N, M, RETRIEVALSYSTEMCAPACITY, MAXPENDING, RETRIEVALCLASSITEMS)

Generate the local-variable state space of a cache node.

n : total number of distinct items m : per-level cache capacity (row vector) retrievalSystemCapacity (optional, default 0): number of items that may be in

the retrieval system simultaneously. When > 0 each emitted row carries the retrieval blocks described below after the cache contents.

maxPending (optional, default 0): maximum number of secondary (delayed-hit)

requests that may be merged onto the in-flight fetches of the cache.

retrievalClassItems (optional, default []): item index served by each

retrieval class, one entry per retrieval class.

Local-variable layout: [cache contents | block A | block B]

cache contents totalCacheCapacity columns holding 1..n item indices block A n columns, one per item; 1 iff a fetch of that item is in

flight (the historical occupancy bitmap, semantics unchanged)

block B one column per retrieval class holding the number of

secondary requests of that class merged onto the in-flight fetch of its item; present only when maxPending > 0

Block B keys the merged requests by retrieval class rather than by item so that the originating job class, hence its hit class, is recoverable when the fetch completes and the merged requests are released as delayed hits.

State.passAndSwap(c, p, G)

[CNEW, DEPCLASS, CHAIN] = PASSANDSWAP(C, P, G)

Applies the pass-and-swap mechanism (Dorsman & Gardner 2024, Queueing Systems 107:205-256, Sect. 2.3) triggered by the service completion of the job in position P of an order-independent/pass-and-swap (PAS) queue.

Inputs:

c - 1 x n ordered state vector of class indices; c(1) is the oldest job. p - position (1..n) of the job whose service token completes. G - (nclasses x nclasses) swapping graph adjacency (G(i,j)~=0 iff a class-i

job may take the place of a class-j job; undirected, self-loops allowed).

Outputs:

cnew - 1 x (n-1) ordered state vector after the pass-and-swap transition. depClass - class index of the job that departs the system. chain - sequence of positions (in the original c) visited by the scan;

chain(1)=p is the vacated slot, c(chain(end)) is the departing job.

Mechanism: starting at position p, the completing job scans backwards (towards newer/later positions) for the first job whose class is swappable with its own per G; it takes that job’s place, ejecting it. The ejected job repeats the scan from its new position. The chain ends when an ejected job finds no swappable successor: that job departs. Classes shift one step along the chain and the head-of-chain slot is removed.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.fromMarginal(sn, ind, n, options)

FROMMARGINAL Generate state space with specific marginal queue lengths

@brief Creates state space where a specific node has given marginal queue lengths @param sn Network structure or Network object @param ind Node index for which to set marginal queue lengths @param n Vector of jobs per class at the specified node @param options Optional structure with configuration parameters @return space Generated state space satisfying marginal constraints

This function generates all possible network states where the specified node (ind) has exactly n(r) jobs of class r, for all classes. It is essential for state space analysis and marginal probability computations.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.tagPad(tags, ntot, R)

TAGS = TAGPAD(TAGS, NTOT, R)

Bring a START/PREEMPT tag matrix up to NTOT rows, one per successor, so that it can be indexed with the same row indices as outspace. Arcs that carry no tag are zero rows, which is why the tag sites only ever write the rows they touch (see State.tagArc).

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.fromMargAndStarted(sn, ind, ntot, stot, options)

FROMMARGANDSTARTED Generate the states with a given TOTAL queue length and a given TOTAL number of started jobs

@brief Class-summed counterpart of State.fromMarginalAndStarted @param sn Network structure or Network object @param ind Node index @param ntot Total number of jobs at the node, all classes summed @param stot Total number of jobs that have started service @param options Optional structure with configuration parameters @return space Generated state space

Where fromMarginalAndStarted takes one per-class vector N and one per-class vector S and builds ONE row, fromMargAndStarted takes only the two totals and returns the union of that row over every (N,S) pair consistent with them: sum(N)=NTOT, sum(S)=STOT, and S <= N elementwise.

Classes disabled at the station are excluded from the enumeration through classcap, for the reason documented in State.fromMarg.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.afterGlobalEvent(sn, ind, glspace, glevent, isSimulation)

[OUTGLSPACE, OUTRATE, OUTPROB, OUTCOMP] = AFTERGLOBALEVENT(QN, IND, GLSPACE, GLEVENT, ISSIMULATION)

OUTCOMP(io) is true when outcome io is a firing completion (a D1 firing of the active mode, which applies the PRE/POST place updates), and false when it is not (e.g. every ENABLE outcome). Callers must not re-derive this from the place markings: a transition whose firing outcome returns exactly what its enabling condition consumed leaves every marking invariant, yet still completes at a nonzero rate.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.afterFJEvent(sn, fjentry, glspace, isSimulation)

[OUTGLOBALSTATES, OUTRATE, OUTPROB] = AFTERFJEVENT(SN, FJENTRY, GLSPACE, ISSIMULATION)

Fire a fork synchronization (one entry of sn.fjsync, built by ModelAdapter.fjtag) on the global state GLSPACE, a cell array with the local state of every stateful node. The firing atomically consumes one parent job of class r held at the (stateful) Fork node and emits one sibling per branch, in the auxiliary classes of the entry’s tag, at the branch head nodes.

Enabling condition (evaluated on the global state):
  1. the Fork holds at least one class-r parent job;

  2. the entry’s tag is the LOWEST free tag for this (fork, class): a tag is free iff its auxiliary classes have zero occupancy network-wide. Canonical lowest-free-tag allocation ensures exactly one fjsync entry per (fork, class) is enabled in any state.

Outputs: OUTGLOBALSTATES is a cell array of successor global states (full glspace cells); OUTRATE the firing rate (immediate) and OUTPROB the probability of each outcome (phase-entry mixtures of the sibling service processes at the branch heads). When ISSIMULATION is true a single outcome is returned, sampled internally.

State.afterEventStationPAS(sn, ind, ist, inspace, event, class, isSimulation, eventCache, R, V, key)

#ok<INUSL> [OUTSPACE, OUTRATE, OUTPROB, EVENTCACHE, OUTSTART, OUTPREEMPT] = AFTEREVENTSTATIONPAS(…)

Event handler for pass-and-swap (PAS) / order-independent (OI) stations.

State layout (see State.fromMarginal): the local state is the full ordered list of class indices c=(c1,…,cn), c1 the oldest job, stored left-aligned in the first W = sn.cap(ist) columns and right zero-padded; the trailing V columns hold routing local variables (carried through unchanged). There is no server/buffer split: service is governed by the total rate function mu(c) (sn.nodeparam{ind}.svcRateFun) and the swapping graph G (sn.nodeparam{ind}.swapGraph), per Dorsman & Gardner (2024), Sect. 2.

  • ARV (passive): a class-class job joins at the back, c -> (c, class), subject to capacity (lost when full).

  • DEP (active): for each position p, the service token of position p fires at rate Delta_mu(c1..cp) = mu(c1..cp) - mu(c1..c_{p-1}); the pass-and-swap mechanism (State.passAndSwap) then determines the departing class. A DEP of class collects all positions whose pass-and-swap ejects a class-class job.

  • PHASE: none (PAS service is exponential).

START/PREEMPT tags. A PAS station has one clock for the whole station and no servers to hold, so “in service” means “at a position whose rate increment Delta_mu is positive”. A job therefore starts service exactly when a position goes from a zero increment to a positive one, which is what sub_startedHere compares. The two canaries bracket the rule: with mu(c) = 1 only the head is served, so a job starts when it reaches the head (M/M/1); with mu(c) = |c| every position is served, so every arrival starts at once (M/M/inf). Under a swap the tag follows the POSITION rather than the job identity, since pass-and-swap redefines which job holds a position. Nothing is ever pushed back out of service here, so OUTPREEMPT is identically zero.

Copyright (c) 2012-2026, Imperial College London All rights reserved.

State.afterEventStation(sn, ind, inspace, event, class, isSimulation, eventCache, M, R, S, phasessz, phaseshift, pie, isf, ismkvmod, ismkvmodclass, lldscaling, lldlimit, cdscaling, hasOnlyExp, ist, K, Ks, mu, phi, proc, capacity, classcap, V, space_buf, space_srv, space_var, key, noPromote)

NOPROMOTE (optional, default false): when true, a DEP at an FCFS-family station does not promote the head-of-buffer job into the vacated server. It is set only for the active (departure) half of an immediate-feedback self-loop (sn.immfeed), so the job that self-loops holds the server and the subsequent passive arrival re-enters service instead of re-queueing behind the waiting jobs. See State.afterEvent and solver_ssa.

State.afterEventHashed(sn, ind, inhash, event, class)

[OUTHASH, OUTRATE, OUTPROB, OUTSTART, OUTPREEMPT] = AFTEREVENTHASHED(QN, IND, INHASH, EVENT, CLASS)

OUTSTART and OUTPREEMPT are the START/PREEMPT annotations of each successor, carried alongside the hashed successor because both CTMC and SSA reach the state machine through here. See State.afterEvent.

State.afterEvent(sn, ind, inspace, event, class, isSimulation, eventCache, ctx, noPromote)

[OUTSPACE, OUTRATE, OUTPROB, EVENTCACHE, OUTSTART, OUTPREEMPT] = AFTEREVENT(QN, IND, INSPACE, EVENT, CLASS, ISSIMULATION, EVENTCACHE, CTX, NOPROMOTE)

OUTSTART and OUTPREEMPT are (rows of OUTSPACE) x (classes) integer matrices holding the START and PREEMPT tags of each successor arc: how many class-r jobs begin holding a server on that arc, and how many are pushed back into the buffer by it. They are annotations on the arcs above, not events: no rate, probability or state depends on them, and a caller that asks for only the first four outputs pays nothing for them.

CTX (optional): loop-invariant context precomputed by State.afterEventInit on the SAME sn passed here (after any caller-side rewrite of nservers/cap/classcap, see solver_ssa preamble). Hot callers pass it to skip the per-call setup below; semantics are identical.

NOPROMOTE (optional, default false): forwarded to State.afterEventStation. When true, a DEP at an FCFS-family station does not promote a waiting job into the vacated server. Set only for the departure half of an immediate-feedback self-loop (sn.immfeed) so the fed-back job holds the server rather than re-queueing. It is part of the cache key below so that immediate-feedback and ordinary departures never collide in the cache.

State.spaceGeneratorNodes(sn, cutoff, options)
State.spaceClosedSingle(M, N, caps)

SS = SPACECLOSEDSINGLE(M, N) SS = SPACECLOSEDSINGLE(M, N, CAPS)

All the ways N identical jobs of ONE class can sit at M nodes. CAPS, when given, is a 1xM per-node bound on the class: the recursion is then pruned AT THE BRANCH rather than after it, so a class that can only occupy one node out of M enumerates M rows instead of nchoosek(N+M-1,M-1).