1function [outspace, outrate, outprob] = afterEventStationSignal(sn, ind, ist, inspace,
class, K, Ks, S, pie, space_buf, space_srv, space_var)
2% [OUTSPACE, OUTRATE, OUTPROB] = AFTEREVENTSTATIONSIGNAL(SN, IND, IST, INSPACE, CLASS, K, KS, S, PIE, SPACE_BUF, SPACE_SRV, SPACE_VAR)
4% Passive arrival of a G-network signal
class at station IST. A signal never
5% joins
the station: it acts on
the jobs already there and
is annihilated.
7% CATASTROPHE empties
the station of every job, ignoring
the removal
count
8% distribution (a catastrophe removes all jobs by definition).
10% NEGATIVE removes a batch of jobs:
11% - Victims. When
the signal declares a target
class (setmodel via
12% forJobClass, sn.signaltarget >= 1) only that
class is eligible;
13% otherwise every non-signal
class is eligible. The untargeted case
is
14%
the classic Gelenbe negative customer and agrees with SolverMAM and
15% SolverLDES, which are both
class-agnostic.
16% - Count. sn.signalremdist gives
the batch-size pmf. An oversized batch
17% empties
the eligible jobs rather than driving
the queue negative, so
18%
the pmf tail
P(B >= n) lumps onto
"remove all n" (
the min(B,n)
19% clipping used by LDES and
the tail term used by MAM).
20% - Policy. sn.signalrempolicy selects
the victim: FCFS removes
the oldest
21% waiting job, LCFS
the newest, RANDOM draws uniformly over waiting and
22% in-service jobs. FCFS/LCFS only touch an in-service job when no job
is
23% waiting (two-tier, as in LDES).
25% The station state carries no arrival-time order for in-service jobs, so
26% when a policy has to reach into
the servers
the victim
is drawn uniformly
27% across
the occupied phases (exact whenever at most one job of
the class
is
28% in service, which covers every single-server station).
30% Copyright (c) 2012-2026, Imperial College London
33[~, nirm, sirm] = State.toMarginal(sn, ind, inspace, K, Ks, space_buf, space_srv, space_var);
35% CATASTROPHE: derived from signaltype as well as
the iscatastrophe flag.
36if State.isCatastropheSignal(sn,
class)
37 outspace = [zeros(1, size(space_buf, 2)), zeros(1, size(space_srv, 2)), space_var];
43% Eligible victim classes.
45if isfield(sn,
'signaltarget') && ~isempty(sn.signaltarget) && numel(sn.signaltarget) >=
class
46 tgt = sn.signaltarget(
class);
51 tgtclasses = find(~sn.issignal(:)
');
53tgtclasses = tgtclasses(nirm(tgtclasses) > 0);
54ntot = sum(nirm(tgtclasses));
55if isempty(tgtclasses) || ntot <= 0
56 outspace = [space_buf, space_srv, space_var]; % no victim: the signal vanishes
62% Batch size pmf, clipped at the number of eligible jobs.
63[kvals, kprobs] = State.signalBatchPMF(sn, class, ntot);
65policy = RemovalPolicy.RANDOM;
66if isfield(sn, 'signalrempolicy
') && ~isempty(sn.signalrempolicy) && numel(sn.signalrempolicy) >= class
67 policy = sn.signalrempolicy(class);
72for ik = 1:numel(kvals)
77 outspace = [outspace; space_buf, space_srv, space_var]; %#ok<AGROW>
78 outprob = [outprob; kprobs(ik)]; %#ok<AGROW>
81 [sp, pr] = removeBatch(sn, ist, space_buf, space_srv, space_var, ...
82 kvals(ik), tgtclasses, policy, K, Ks, S, pie);
83 outspace = [outspace; sp]; %#ok<AGROW>
84 outprob = [outprob; kprobs(ik) * pr]; %#ok<AGROW>
87% Merge duplicate destination states so the generator sees one entry each.
88[outspace, outprob] = mergeStates(outspace, outprob);
89outrate = -1 * ones(size(outspace, 1), 1); % passive action
92function [outspace, outprob] = removeBatch(sn, ist, buf, srv, var, k, tgtclasses, policy, K, Ks, S, pie)
93% Remove k jobs one at a time; sequential uniform draws without replacement
94% reproduce a uniform choice of the removed subset.
95outspace = [buf, srv, var];
100 for row = 1:size(outspace, 1)
101 b = outspace(row, 1:size(buf, 2));
102 s = outspace(row, size(buf, 2) + (1:size(srv, 2)));
103 v = outspace(row, size(buf, 2) + size(srv, 2) + 1:end);
104 [sp, pr] = removeOne(sn, ist, b, s, v, tgtclasses, policy, K, Ks, S, pie);
106 % nothing left to remove: the state is already drained
107 nextspace = [nextspace; outspace(row, :)]; %#ok<AGROW>
108 nextprob = [nextprob; outprob(row)]; %#ok<AGROW>
110 nextspace = [nextspace; sp]; %#ok<AGROW>
111 nextprob = [nextprob; outprob(row) * pr]; %#ok<AGROW>
114 [outspace, outprob] = mergeStates(nextspace, nextprob);
118function [outspace, outprob] = removeOne(sn, ist, buf, srv, var, tgtclasses, policy, K, Ks, S, pie)
119% Enumerate the single-victim outcomes and their probabilities.
123[waitPos, waitClass, waitWeight, isOrdered, isPairBuf] = waitingVictims(sn, ist, buf, tgtclasses);
124[srvClass, srvPhase, srvCount] = inServiceVictims(srv, tgtclasses, K, Ks);
125nwait = sum(waitWeight);
127if nwait == 0 && nsrv == 0
131% FCFS/LCFS rank the waiting line by age, which only an ordered buffer
132% records; a per-class count buffer (SIRO/SEPT/LEPT) carries no age, so an
133% age-based policy degenerates to a uniform draw over the waiting jobs.
134ageOrdered = isOrdered && (policy == RemovalPolicy.FCFS || policy == RemovalPolicy.LCFS);
135if ageOrdered && nwait > 0
136 if policy == RemovalPolicy.FCFS
137 [~, pick] = max(waitPos); % head of line: the last occupied slot
139 [~, pick] = min(waitPos); % most recent arrival: the first occupied slot
141 [b2, s2] = dropWaiting(buf, srv, waitPos(pick), isOrdered, isPairBuf, waitClass(pick));
142 outspace = [b2, s2, var];
147if policy == RemovalPolicy.RANDOM
148 total = nwait + nsrv; % uniform over waiting and in-service jobs alike
150 total = nwait; % FCFS/LCFS drain the waiting line before the servers
157 for w = 1:numel(waitPos)
158 [b2, s2] = dropWaiting(buf, srv, waitPos(w), isOrdered, isPairBuf, waitClass(w));
159 outspace = [outspace; b2, s2, var]; %#ok<AGROW>
160 outprob = [outprob; waitWeight(w) / total]; %#ok<AGROW>
163if policy == RemovalPolicy.RANDOM || nwait == 0
164 for j = 1:numel(srvClass)
168 [b2, s2] = dropInService(sn, ist, buf, srv, srvClass(j), srvPhase(j), K, Ks, S, pie, isOrdered, isPairBuf);
169 outspace = [outspace; b2, s2, var]; %#ok<AGROW>
170 outprob = [outprob; srvCount(j) / total]; %#ok<AGROW>
173[outspace, outprob] = mergeStates(outspace, outprob);
176function [pos, cls, weight, isOrdered, isPairBuf] = waitingVictims(sn, ist, buf, tgtclasses)
177% Eligible waiting jobs, as (position, class, multiplicity). Three buffer
178% layouts are in use: an ordered list of class ids (FCFS family), an ordered
179% list of (class, phase) pairs (preemptive family), and a per-class count
180% vector (SIRO/SEPT/LEPT). Stations that admit every job into service have
181% no waiting line at all. Only the ordered layouts record arrival order.
191 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO}
194 if buf(c) > 0 && ismember(buf(c), tgtclasses)
195 pos(end+1) = c; %#ok<AGROW>
196 cls(end+1) = buf(c); %#ok<AGROW>
197 weight(end+1) = 1; %#ok<AGROW>
200 case {SchedStrategy.FCFSPR, SchedStrategy.FCFSPI, SchedStrategy.FCFSPRPRIO, SchedStrategy.FCFSPIPRIO, ...
201 SchedStrategy.LCFSPR, SchedStrategy.LCFSPI, SchedStrategy.LCFSPRPRIO, SchedStrategy.LCFSPIPRIO}
204 for c = 1:2:numel(buf)
205 if buf(c) > 0 && ismember(buf(c), tgtclasses)
206 pos(end+1) = c; %#ok<AGROW>
207 cls(end+1) = buf(c); %#ok<AGROW>
208 weight(end+1) = 1; %#ok<AGROW>
211 case {SchedStrategy.SIRO, SchedStrategy.SEPT, SchedStrategy.LEPT}
212 % per-class counts: each eligible class is one victim kind carrying
213 % as many interchangeable jobs as its count
215 if r <= numel(buf) && buf(r) > 0
216 pos(end+1) = r; %#ok<AGROW>
217 cls(end+1) = r; %#ok<AGROW>
218 weight(end+1) = buf(r); %#ok<AGROW>
222 % PS/INF/DPS/GPS/LPS and the source hold no waiting jobs
226function [cls, phase, count] = inServiceVictims(srv, tgtclasses, K, Ks)
234 cls(end+1) = r; %#ok<AGROW>
235 phase(end+1) = p; %#ok<AGROW>
236 count(end+1) = n; %#ok<AGROW>
242function [buf, srv] = dropWaiting(buf, srv, pos, isOrdered, isPairBuf, cls)
243% Remove a waiting job, keeping the layout invariant (empty slots pad the
244% left, the head of line stays rightmost).
246 buf([pos, pos+1]) = [];
252 buf(cls) = buf(cls) - 1; % per-class count buffer
256function [buf, srv] = dropInService(sn, ist, buf, srv, cls, phase, K, Ks, S, pie, isOrdered, isPairBuf)
257% Remove an in-service job and, at a station that keeps a waiting line, pull
258% the head of line into the freed server.
259srv(Ks(cls) + phase) = srv(Ks(cls) + phase) - 1;
260if isempty(buf) || sum(srv) >= S(ist)
264 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO}
265 headpos = find(buf > 0, 1, 'last
');
267 promo = buf(headpos);
268 % the slot is vacated, not blanked in place: the waiting line
269 % stays right-aligned with the empty slots padding the left
272 srv(Ks(promo) + 1) = srv(Ks(promo) + 1) + 1;
274 case {SchedStrategy.FCFSPR, SchedStrategy.FCFSPI, SchedStrategy.FCFSPRPRIO, SchedStrategy.FCFSPIPRIO, ...
275 SchedStrategy.LCFSPR, SchedStrategy.LCFSPI, SchedStrategy.LCFSPRPRIO, SchedStrategy.LCFSPIPRIO}
277 for c = 1:2:numel(buf)
283 promo = buf(headpos);
284 promophase = buf(headpos+1);
288 buf([headpos, headpos+1]) = [];
290 srv(Ks(promo) + promophase) = srv(Ks(promo) + promophase) + 1; % resumes at its stored phase
292 case {SchedStrategy.SIRO, SchedStrategy.SEPT, SchedStrategy.LEPT}
293 % the freed server takes a waiting job; the count buffer carries no
294 % order, so any waiting class is equally eligible. Promote the
295 % lowest-indexed waiting class to keep the map single-valued: the
296 % SIRO service order is itself resolved by the rate function.
297 promo = find(buf > 0, 1, 'first
');
299 buf(promo) = buf(promo) - 1;
300 srv(Ks(promo) + 1) = srv(Ks(promo) + 1) + 1;
303 % no waiting line to promote from
307function [space, prob] = mergeStates(space, prob)
311[u, ~, ic] = unique(space, 'rows
', 'stable
');
312p = zeros(size(u, 1), 1);
314 p(ic(i)) = p(ic(i)) + prob(i);