LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
afterEventStation.m
1function [outspace, outrate, outprob, eventCache] = afterEventStation(sn, ind, inspace, event, class, isSimulation, eventCache, ...
2 M, R, S, phasessz, phaseshift, pie, isf, ismkvmod, ismkvmodclass, lldscaling, lldlimit, cdscaling, ...
3 hasOnlyExp, ist, K, Ks, mu, phi, proc, capacity, classcap, V, space_buf, space_srv, space_var, key, noPromote)
4% NOPROMOTE (optional, default false): when true, a DEP at an FCFS-family
5% station does not promote the head-of-buffer job into the vacated server.
6% It is set only for the active (departure) half of an immediate-feedback
7% self-loop (sn.immfeed), so the job that self-loops holds the server and the
8% subsequent passive arrival re-enters service instead of re-queueing behind
9% the waiting jobs. See State.afterEvent and solver_ssa.
10if nargin < 34 || isempty(noPromote)
11 noPromote = false;
12end
13% A retrial station keeps blocked arrivals in an orbit (represented by the
14% buffer slots) instead of an ordered waiting line: on service completion the
15% freed server is NOT filled from the orbit (no promotion); orbiting jobs
16% re-enter only through RETRY events at the memoryless retrial rate.
17isRetrialStation = isfield(sn,'retrialProc') && ~isempty(sn.retrialProc) ...
18 && ist > 0 && any(~cellfun(@isempty, sn.retrialProc(ist,:)));
19outspace = [];
20outrate = [];
21outprob = 1;
22% Pass-and-swap / order-independent stations use a dedicated ordered-list
23% representation and rate function mu(c); handle them separately.
24if sn.sched(ist) == SchedStrategy.PAS
25 [outspace, outrate, outprob, eventCache] = State.afterEventStationPAS(sn, ind, ist, inspace, event, class, isSimulation, eventCache, R, V, key);
26 return;
27end
28% Server breakdown. The status is the trailing local-variable column (0 = down,
29% 1 = up), exclusive with the BAS marker and the polling controller. A down
30% server does not serve, so DEP and PHASE are suppressed unless a degraded
31% down-server rate was configured, in which case the completion rate is rescaled
32% by downRate/upRate at the end of the handler. Gating here keeps every
33% scheduling branch below unaware of the server status.
34isBreakdownStation = isfield(sn,'hasbreakdown') && ~isempty(sn.hasbreakdown) ...
35 && numel(sn.hasbreakdown) >= ind && sn.hasbreakdown(ind) == 1;
36downRateScale = 1;
37if isBreakdownStation && ~isempty(inspace) && (event == EventType.DEP || event == EventType.PHASE)
38 if inspace(1,end) == 0 % server down
39 downRate = 0;
40 if ~isempty(sn.downServiceRates) && size(sn.downServiceRates,1) >= ist ...
41 && size(sn.downServiceRates,2) >= class
42 downRate = sn.downServiceRates(ist,class);
43 end
44 if downRate <= 0
45 outspace = [];
46 outrate = [];
47 outprob = [];
48 return
49 end
50 upRate = sn.rates(ist,class);
51 if ~isfinite(upRate) || upRate <= 0
52 line_error(mfilename, sprintf(['Station ''%s'' declares a down-server service rate for class ''%s'' but has ' ...
53 'no finite up-server service rate to rescale.'], sn.nodenames{ind}, sn.classnames{class}));
54 end
55 downRateScale = downRate / upRate;
56 end
57end
58switch event
59 case EventType.FAILURE
60 % An up server fails at the memoryless rate breakdownMu, whether or not
61 % it is serving. Only the status column changes: jobs in service are not
62 % lost and, service being memoryless here, they resume on repair. The
63 % passive half of the synchronization is LOCAL, so no job moves.
64 outspace = [];
65 outrate = [];
66 outprob = [];
67 if isBreakdownStation && ~isempty(inspace) && inspace(1,end) == 1
68 outspace = inspace;
69 outspace(:,end) = 0;
70 outrate = sn.breakdownMu(ist);
71 outprob = 1;
72 end
73 case EventType.REPAIR
74 % A down server is restored at the memoryless rate repairMu.
75 outspace = [];
76 outrate = [];
77 outprob = [];
78 if isBreakdownStation && ~isempty(inspace) && inspace(1,end) == 0
79 outspace = inspace;
80 outspace(:,end) = 1;
81 outrate = sn.repairMu(ist);
82 outprob = 1;
83 end
84 case EventType.ARV %% passive
85 % A Place holds a marking, not a service facility: it has no servers and
86 % no service phases, so an arriving token only increments the class
87 % marking. The scheduling branches below would instead write an entering
88 % job into a phase slot that a Place state does not carry; MATLAB grows
89 % the row to fit, and the widened state can no longer be matched against
90 % the marking state space, so the successor hash fails and the arrival is
91 % dropped. Closed SPNs never notice, because there tokens reach a Place
92 % through FIRE (State.afterGlobalEvent) rather than through ARV.
93 if sn.nodetype(ind) == NodeType.Place
94 outspace = inspace;
95 outspace(:,class) = outspace(:,class) + 1;
96 % passive action: the rate is set by the active node
97 outrate = -1*ones(size(outspace,1),1);
98 outprob = ones(size(outspace,1),1);
99 return
100 end
101 % Signal / catastrophe arrival: a signal class is a G-network negative
102 % customer. Instead of joining, it removes job(s) of its target class
103 % from this station and is itself annihilated. CATASTROPHE empties the
104 % station of all jobs. The event stays passive (rate set by the active
105 % signal source); when there is no target job the signal simply vanishes
106 % (destination state unchanged).
107 if isfield(sn,'issignal') && ~isempty(sn.issignal) && sn.issignal(class)
108 [outspace, outrate, outprob] = State.afterEventStationSignal(sn, ind, ist, inspace, class, K, Ks, S, pie, space_buf, space_srv, space_var);
109 % Which job a signal removes is in general a random choice, so the
110 % generator needs every destination and its probability. A simulation
111 % instead walks one sample path, so it draws a single successor from
112 % outprob, as the balking branch below does for its passive action.
113 if isSimulation && size(outprob,1) > 1
114 cum_prob = cumsum(outprob) / sum(outprob);
115 firing_ctr = 1 + max([0,find( rand > cum_prob' )]); % select action
116 outspace = outspace(firing_ctr,:);
117 outrate = -1;
118 outprob = 1;
119 end
120 return;
121 end
122 % Ordinary SPN Place arrival: see _kb/11-conventions-and-gotchas.md
123 % ("An ordinary Place must be special-cased before the generic
124 % scheduling switch") for the rationale.
125 isOrdinaryPlace = isfield(sn,'nodetype') && ~isempty(sn.nodetype) ...
126 && sn.nodetype(ind) == NodeType.Place;
127 if isOrdinaryPlace && isfield(sn,'isqueueingplace') && ~isempty(sn.isqueueingplace) ...
128 && ist <= numel(sn.isqueueingplace) && sn.isqueueingplace(ist)
129 isOrdinaryPlace = false;
130 end
131 if isOrdinaryPlace
132 outspace = inspace;
133 nrows = size(outspace,1);
134 if inspace(1,class) < classcap(ist,class)
135 outspace(:,class) = outspace(:,class) + 1;
136 outrate = -ones(nrows,1); % passive: rate set by active source
137 outprob = ones(nrows,1);
138 else
139 % place full: arrival is blocked and lost (no state change)
140 outrate = -ones(nrows,1);
141 outprob = zeros(nrows,1);
142 end
143 return;
144 end
145 % return if there is no space to accept the arrival
146 [ni,nir] = State.toMarginalAggr(sn,ind,inspace,K,Ks,space_buf,space_srv,space_var);
147 % otherwise check scheduling strategy
148 pentry = pie{ist}{class};
149 % For Place nodes (INF scheduling with NaN service), use uniform entry probability
150 if all(isnan(pentry))
151 pentry = ones(size(pentry)) / length(pentry);
152 end
153 outprob = [];
154 outprob_k = [];
155 for kentry = 1:K(class)
156 space_var_k = space_var;
157 space_srv_k = space_srv;
158 space_buf_k = space_buf;
159 switch sn.sched(ist)
160 case SchedStrategy.EXT % source, can receive any "virtual" arrival from the sink as long as it is from an open class
161 if isinf(sn.njobs(class))
162 outspace = inspace;
163 outrate = -1*zeros(size(outspace,1)); % passive action, rate is unspecified
164 outprob = ones(size(outspace,1));
165 break
166 end
167 case {SchedStrategy.PS, SchedStrategy.INF, SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, SchedStrategy.LPS}
168 % job enters service immediately
169 if space_srv_k(:,Ks(class)+kentry) < classcap(ist,class)
170 space_srv_k(:,Ks(class)+kentry) = space_srv_k(:,Ks(class)+kentry) + 1;
171 outprob_k = pentry(kentry)*ones(size(space_srv_k,1));
172 else
173 outprob_k = pentry(kentry)*zeros(size(space_srv_k,1));
174 end
175 case {SchedStrategy.SIRO, SchedStrategy.SEPT, SchedStrategy.LEPT}
176 % Idle-server test on server occupancy, not total count ni: these
177 % agree for work-conserving states but an immediate-feedback
178 % self-loop transiently yields an idle server with a non-empty
179 % buffer, where the fed-back job must re-enter the vacated server
180 % (mirrors the FCFS case below).
181 if sum(space_srv_k,2)<S(ist)
182 space_srv_k(:,Ks(class)+kentry) = space_srv_k(:,Ks(class)+kentry) + 1;
183 outprob_k = pentry(kentry)*ones(size(space_srv_k,1));
184 else
185 space_buf_k(:,class) = space_buf_k(:,class) + 1;
186 outprob_k = pentry(kentry)*ones(size(space_srv_k,1));
187 end
188 case SchedStrategy.POLLING
189 % The controller, not the arrival, decides who is served:
190 % an arriving job joins its class buffer and waits for the
191 % server to walk to it, even when the facility is idle,
192 % because the server is then in a switchover. The exception
193 % is a parked server, which State.pollingNext only ever
194 % produces with an empty station and immediate switchovers:
195 % it therefore reaches the arriving job in zero time and
196 % starts a visit on it at once.
197 pinfoA = State.pollingInfo(sn, ind);
198 srvclassA = 0;
199 for rA=1:R
200 if sum(space_srv_k(1,(Ks(rA)+1):(Ks(rA)+K(rA)))) > 0
201 srvclassA = rA;
202 break
203 end
204 end
205 [posA, swkA] = State.pollingGet(pinfoA, space_var_k, srvclassA);
206 if srvclassA == 0 && swkA == 0
207 % Parked. The arriving job is the only work present, so
208 % the walk necessarily resolves to a visit on its own
209 % class: it enters service here, in phase kentry, and
210 % never occupies the buffer.
211 nbufA = space_buf_k(1,1:R);
212 nbufA(class) = nbufA(class) + 1;
213 [qA, ~, budgetA] = State.pollingNext(pinfoA, posA, nbufA, R, true);
214 space_srv_k(:,Ks(class)+kentry) = space_srv_k(:,Ks(class)+kentry) + 1;
215 space_var_k = State.pollingSet(pinfoA, space_var_k, qA, 0, budgetA);
216 else
217 space_buf_k(:,class) = space_buf_k(:,class) + 1;
218 end
219 outprob_k = pentry(kentry)*ones(size(space_srv_k,1));
220 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.LCFS, SchedStrategy.LCFSPRIO}
221 % find states with all servers busy - this
222 % needs not to be moved
223
224 % if MAP service, when empty restart from the phase
225 % stored in space_var for this class
226 if ~ismkvmodclass(class) || (ismkvmodclass(class) && kentry == space_var(sum(sn.nvars(ind,1:class))))
227 if ismkvmodclass(class)
228 pentry = zeros(size(pentry));
229 pentry(kentry) = 1.0;
230 end
231 all_busy_srv = sum(space_srv_k,2) >= S(ist);
232
233 % find and modify states with an idle server
234 idle_srv = sum(space_srv_k,2) < S(ist);
235 space_srv_k(idle_srv, end-sum(K)+Ks(class)+kentry) = space_srv_k(idle_srv,end-sum(K)+Ks(class)+kentry) + 1; % job enters service
236
237 % this section dynamically grows the number of
238 % elements in the buffer
239
240 if any(ni < capacity(ist))
241 if any(nir(:,class) < classcap(ist,class)) % if there is room
242 if ~any(space_buf_k(:)==0) % but the buffer has no empty slots
243 % append job slot
244 space_buf_k = [zeros(size(space_buf_k,1),1),space_buf_k];
245 end
246 end
247 end
248 %end
249 %get position of first empty slot
250 empty_slots = -1*ones(size(all_busy_srv,1),1);
251 if size(space_buf_k,2) == 0
252 empty_slots(all_busy_srv) = false;
253 elseif size(space_buf_k,2) == 1
254 empty_slots(all_busy_srv) = space_buf_k(all_busy_srv,:)==0;
255 else
256 empty_slots(all_busy_srv) = max(bsxfun(@times, space_buf_k(all_busy_srv,:)==0, [1:size(space_buf_k,2)]),[],2);
257 end
258
259 % ignore states where the buffer has no empty slots.
260 % A structurally free buffer column is NOT enough: the job may
261 % only be placed when the CAPACITY also permits it. Without this
262 % the job was written into the slot and the whole row was then
263 % vetoed by the capacity filter below (en_o), which returned an
264 % EMPTY outspace -- i.e. the arrival event never fired, so the
265 % upstream departure was never counted and the reported arrival
266 % rate collapsed from the OFFERED rate to the CARRIED one
267 % (BUG-85: M/M/1/1 reported ArvR 0.444444 instead of 0.800000,
268 % hiding the loss entirely, while M/M/1/2 -- whose buffer is
269 % full rather than absent -- correctly reported 0.800000).
270 % Leaving the state unchanged instead makes the arrival a
271 % self-loop: the event fires, ArvR counts the offered job, and
272 % the job is lost. A self-loop cancels on the generator diagonal,
273 % so the stationary distribution -- and hence QLen/Util/Tput --
274 % cannot move.
275 % Gate the placement on the capacity ONLY for a PHYSICAL
276 % finite capacity. When the bound is a state-space cutoff
277 % (open class, no physical cap; solver_ssa folds the cutoff
278 % into the capacity/classcap arguments), keep the pre-change
279 % behaviour: place the job structurally and let the en_o
280 % capacity filter below delete the beyond-cutoff row
281 % (truncation). Firing the capacity gate at a cutoff would
282 % send the arrival to the loss/block branch, turning a
283 % truncation into a self-loop. See State.isPhysicalCapacity.
284 if State.isPhysicalCapacity(sn, ist, class)
285 hasRoom = (ni < capacity(ist)) & (nir(:,class) < classcap(ist,class));
286 else
287 hasRoom = true(size(empty_slots));
288 end
289 wbuf_empty = empty_slots>0 & hasRoom;
290 if any(wbuf_empty)
291 space_srv_k = space_srv_k(wbuf_empty,:);
292 space_buf_k = space_buf_k(wbuf_empty,:);
293 space_var_k = space_var_k(wbuf_empty,:);
294 empty_slots = empty_slots(wbuf_empty);
295 space_buf_k(sub2ind(size(space_buf_k),1:size(space_buf_k,1),empty_slots')) = class;
296 %outspace(all_busy_srv(wbuf_empty),:) = [space_buf, space_srv, space_var];
297 elseif any(all_busy_srv) && ~State.arrivalIsLost(sn, ist, class)
298 % The arrival cannot be placed (all servers busy, no room).
299 % Whether that is a LOSS or a BLOCK is decided by the class
300 % type, not by the drop rule: a CLOSED job has nowhere to go,
301 % so it cannot be lost, and an explicit blocking rule
302 % (BAS/BBS/RSRD) asks for blocking too. Removing the rows
303 % returns an EMPTY outspace, which disables the upstream
304 % departure until space frees (and is what the CTMC
305 % become-blocked edge tests for, so true BAS fires). For a
306 % LOST arrival the rows are kept unchanged instead, so the
307 % event still fires and ArvR counts the offered job -- see
308 % State.arrivalIsLost and the hasRoom comment above.
309 space_srv_k = space_srv_k(idle_srv,:);
310 space_buf_k = space_buf_k(idle_srv,:);
311 space_var_k = space_var_k(idle_srv,:);
312 end
313 outprob_k = pentry(kentry)*ones(size(space_srv_k,1),1);
314 else
315 outprob_k = 0*ones(size(space_srv_k,1),1); % zero probability event
316 end
317 case {SchedStrategy.FCFSPR,SchedStrategy.FCFSPI,SchedStrategy.FCFSPRPRIO,SchedStrategy.FCFSPIPRIO,SchedStrategy.LCFSPR,SchedStrategy.LCFSPI,SchedStrategy.LCFSPRPRIO,SchedStrategy.LCFSPIPRIO}
318 % find states with all servers busy - this
319 % must not be moved
320 all_busy_srv = sum(space_srv_k,2) >= S(ist);
321 % find states with an idle server
322 idle_srv = sum(space_srv_k,2) < S(ist);
323
324 % reorder states so that idle ones come first
325 space_buf_k_reord = space_buf_k(idle_srv,:);
326 space_srv_k_reord = space_srv_k(idle_srv,:);
327 space_var_k_reord = space_var_k(idle_srv,:);
328
329 % if idle, the job enters service in phase kentry
330 if any(idle_srv)
331 space_srv_k_reord(:, end-sum(K)+Ks(class)+kentry) = space_srv_k_reord(:,end-sum(K)+Ks(class)+kentry) + 1;
332 outprob_k = pentry(kentry);
333 else
334 % if all busy, expand output states for all possible choices of job class to preempt
335 psentry = ones(size(space_buf_k_reord,1),1); % probability scaling due to preemption
336 isPrioAware = (sn.sched(ist) == SchedStrategy.FCFSPRPRIO || sn.sched(ist) == SchedStrategy.FCFSPIPRIO || sn.sched(ist) == SchedStrategy.LCFSPRPRIO || sn.sched(ist) == SchedStrategy.LCFSPIPRIO);
337 isLcfsPrioFamily = (sn.sched(ist) == SchedStrategy.LCFSPRPRIO || sn.sched(ist) == SchedStrategy.LCFSPIPRIO);
338 for classpreempt = 1:R
339 % For priority variants (or LCFSPR/FCFSPR when priorities differ),
340 % only higher-priority jobs can preempt
341 isPrioSched = (sn.sched(ist) == SchedStrategy.FCFSPRPRIO || sn.sched(ist) == SchedStrategy.FCFSPIPRIO || sn.sched(ist) == SchedStrategy.LCFSPRPRIO || sn.sched(ist) == SchedStrategy.LCFSPIPRIO);
342 % Priority-awareness is a property of the DECLARED policy, never of
343 % the data. LCFSPRPRIO/FCFSPRPRIO are the priority-aware variants;
344 % inferring the discipline from ~all(classprio==classprio(1)) turned
345 % plain LCFSPR/FCFSPR into something that is neither the base policy
346 % nor the PRIO variant. The FCFSPR case below states the rule outright
347 % ("FCFS preempt-resume (no priority)").
348 isPrioAware = isPrioSched;
349 if isPrioAware
350 % Across priority groups a strictly higher-priority arrival preempts.
351 % WITHIN a group the base discipline decides: LCFS-PR keeps the NEWEST
352 % job in service, so an equal-priority arrival preempts; FCFS-PR never
353 % lets an arrival preempt. Refusing on equality for both left
354 % LCFSPRPRIO unable to preempt anything when the priorities were flat,
355 % so no arrival could enter a busy station at all.
356 isLcfsPrioFamily = (sn.sched(ist) == SchedStrategy.LCFSPRPRIO || sn.sched(ist) == SchedStrategy.LCFSPIPRIO);
357 if isLcfsPrioFamily
358 cannotPreempt = sn.classprio(class) > sn.classprio(classpreempt);
359 else
360 cannotPreempt = sn.classprio(class) >= sn.classprio(classpreempt);
361 end
362 if cannotPreempt
363 continue;
364 end
365 end
366 for phasepreempt = 1:K(classpreempt) % phase of job to preempt
367 si_preempt = space_srv_k(:, (end-sum(K)+Ks(classpreempt)+phasepreempt));
368 busy_preempt = si_preempt > 0; % states where there is at least on class-r job in execution
369 if any(busy_preempt)
370 psentry = [psentry; si_preempt(busy_preempt) ./ sum(space_srv_k,2)];
371 space_srv_k_preempt = space_srv_k(busy_preempt,:);
372 space_buf_k_preempt = space_buf_k(busy_preempt,:);
373 space_var_k_preempt = space_var_k(busy_preempt,:);
374 space_srv_k_preempt(:, end-sum(K)+Ks(classpreempt)+phasepreempt) = space_srv_k_preempt(:,end-sum(K)+Ks(classpreempt)+phasepreempt) - 1; % remove preempted job
375 space_srv_k_preempt(:, end-sum(K)+Ks(class)+kentry) = space_srv_k_preempt(:,end-sum(K)+Ks(class)+kentry) + 1;
376
377 % dynamically grow buffer lenght in
378 % simulation
379 if isSimulation
380 if ni < capacity(ist) && nir(class) < classcap(ist,class) % if there is room
381 if ~any(space_buf_k_preempt(:)==0) % but the buffer has no empty slots
382 % append job slot
383 space_buf_k_preempt = [zeros(size(space_buf_k_preempt,1),2),space_buf_k]; % append two columns for (class, preempt-phase)
384 end
385 end
386 end
387
388 %get position of first empty slot
389 empty_slots = -1*ones(sum(busy_preempt),1);
390 if size(space_buf_k_preempt,2) == 0
391 empty_slots(busy_preempt) = false;
392 elseif size(space_buf_k_preempt,2) == 2 % 2 due to (class, preempt-phase) pairs
393 empty_slots(busy_preempt) = space_buf_k_preempt(busy_preempt,1:2:end)==0;
394 else
395 empty_slots(busy_preempt) = max(bsxfun(@times, space_buf_k_preempt(busy_preempt,:)==0, [1:size(space_buf_k_preempt,2)]),[],2)-1; %-1 due to (class, preempt-phase) pairs
396 end
397
398 % ignore states where the buffer has no empty slots
399 wbuf_empty = empty_slots>0;
400 if any(wbuf_empty)
401 space_srv_k_preempt = space_srv_k_preempt(wbuf_empty,:);
402 space_buf_k_preempt = space_buf_k_preempt(wbuf_empty,:);
403 space_var_k_preempt = space_var_k_preempt(wbuf_empty,:);
404 empty_slots = empty_slots(wbuf_empty);
405 if sn.sched(ist) == SchedStrategy.LCFSPR || sn.sched(ist) == SchedStrategy.LCFSPRPRIO || sn.sched(ist) == SchedStrategy.FCFSPR || sn.sched(ist) == SchedStrategy.FCFSPRPRIO % preempt-resume
406 space_buf_k_preempt(sub2ind(size(space_buf_k_preempt),1:size(space_buf_k_preempt,1),empty_slots')+1) = phasepreempt;
407 elseif sn.sched(ist) == SchedStrategy.LCFSPI || sn.sched(ist) == SchedStrategy.LCFSPIPRIO || sn.sched(ist) == SchedStrategy.FCFSPI || sn.sched(ist) == SchedStrategy.FCFSPIPRIO % preempt-independent
408 space_buf_k_preempt(sub2ind(size(space_buf_k_preempt),1:size(space_buf_k_preempt,1),empty_slots')+1) = 1;
409 end
410 space_buf_k_preempt(sub2ind(size(space_buf_k_preempt),1:size(space_buf_k_preempt,1),empty_slots')) = classpreempt;
411 %outspace(all_busy_srv(wbuf_empty),:) = [space_buf, space_srv, space_var];
412 end
413 space_srv_k_reord = [space_srv_k_reord; space_srv_k_preempt];
414 space_buf_k_reord = [space_buf_k_reord; space_buf_k_preempt];
415 space_var_k_reord = [space_var_k_reord; space_var_k_preempt];
416 end
417 end
418 end
419
420 % Rows where the arrival can preempt nothing: every busy server holds a
421 % job it may not displace. The job cannot seize a server, so it WAITS in
422 % the buffer. Without this branch the loop above emits no state at all
423 % for those rows -- the arrival transition simply does not exist, so the
424 % class can never enter a busy station and its queue is silently
425 % understated. Python already carries this fallback (its BUG-70 fix);
426 % MATLAB never received it. The job is stored as a (class, entry-phase)
427 % pair exactly as a preempted job is, so on promotion it resumes from
428 % that phase (for exponential service resume == restart).
429 if isPrioAware
430 canPreempt = false(size(space_srv_k,1),1);
431 for cp = 1:R
432 if isLcfsPrioFamily
433 preemptable = sn.classprio(class) <= sn.classprio(cp);
434 else
435 preemptable = sn.classprio(class) < sn.classprio(cp);
436 end
437 if preemptable
438 for ph = 1:K(cp)
439 canPreempt = canPreempt | (space_srv_k(:, end-sum(K)+Ks(cp)+ph) > 0);
440 end
441 end
442 end
443 waitRows = all_busy_srv & ~canPreempt;
444 if any(waitRows)
445 space_srv_k_wait = space_srv_k(waitRows,:);
446 space_buf_k_wait = space_buf_k(waitRows,:);
447 space_var_k_wait = space_var_k(waitRows,:);
448 % rightmost empty (class,phase) pair, matching the preemption store
449 empty_wait = -1*ones(sum(waitRows),1);
450 if size(space_buf_k_wait,2) > 0
451 empty_wait = max(bsxfun(@times, space_buf_k_wait==0, 1:size(space_buf_k_wait,2)),[],2)-1;
452 end
453 keep_wait = empty_wait > 0;
454 if any(keep_wait)
455 space_srv_k_wait = space_srv_k_wait(keep_wait,:);
456 space_buf_k_wait = space_buf_k_wait(keep_wait,:);
457 space_var_k_wait = space_var_k_wait(keep_wait,:);
458 ew = empty_wait(keep_wait);
459 nw = size(space_buf_k_wait,1);
460 space_buf_k_wait(sub2ind(size(space_buf_k_wait),(1:nw)',ew)) = class;
461 space_buf_k_wait(sub2ind(size(space_buf_k_wait),(1:nw)',ew+1)) = kentry;
462 psentry = [psentry; ones(nw,1)];
463 space_srv_k_reord = [space_srv_k_reord; space_srv_k_wait];
464 space_buf_k_reord = [space_buf_k_reord; space_buf_k_wait];
465 space_var_k_reord = [space_var_k_reord; space_var_k_wait];
466 end
467 end
468 end
469 outprob_k = pentry(kentry) * psentry .* ones(size(space_srv_k_reord,1),1);
470 end
471 space_buf_k = space_buf_k_reord; % save reordered output states
472 space_srv_k = space_srv_k_reord; % save reordered output states
473 space_var_k = space_var_k_reord; % save reordered output states
474 end
475 % form the new state
476 outspace_k = [space_buf_k, space_srv_k, space_var_k];
477 % remove states where new arrival violates capacity or cutoff constraints
478 [oi,oir] = State.toMarginalAggr(sn,ind,outspace_k,K,Ks,space_buf_k,space_srv_k,space_var_k);
479 en_o = classcap(ist,class)>= oir(:,class) & capacity(ist)*ones(size(oi,1),1) >= oi;
480
481 if size(outspace,2)>size(outspace_k(en_o,:),2)
482 outspace = [outspace; zeros(1,size(outspace,2)-size(outspace_k(en_o,:),2)),outspace_k(en_o,:)];
483 elseif size(outspace,2)<size(outspace_k(en_o,:),2)
484 outspace = [zeros(size(outspace,1),size(outspace_k(en_o,:),2)-size(outspace,2)), outspace; outspace_k(en_o,:)];
485 else
486 outspace = [outspace; outspace_k(en_o,:)];
487 end
488 outrate = [outrate; -1*ones(size(outspace_k(en_o,:),1),1)]; % passive action, rate is unspecified
489 outprob = [outprob; outprob_k(en_o,:)];
490 end
491 % Balking (QUEUE_LENGTH strategy): with probability balkProb the
492 % arriving class-r job refuses to join, based on the pre-arrival total
493 % station population ni. A balked job is lost (destination state left
494 % unchanged); the admitted branches are scaled by (1-balkProb). Only
495 % the QUEUE_LENGTH strategy is a pure function of the state vector and
496 % is therefore admissible in state-space solvers; EXPECTED_WAIT /
497 % COMBINED depend on the mean wait and are rejected in the analyzers.
498 if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy) ...
499 && sn.balkingStrategy(ist,class) == BalkingStrategy.QUEUE_LENGTH ...
500 && ~isempty(outspace)
501 balkProb = 0;
502 thresholds = sn.balkingThresholds{ist,class};
503 qlen = ni(1); % pre-arrival total station population
504 for ti = 1:numel(thresholds)
505 th = thresholds{ti};
506 if qlen >= th{1} && qlen <= th{2}
507 balkProb = th{3};
508 break
509 end
510 end
511 if balkProb > 0
512 outprob = outprob * (1 - balkProb);
513 % balked branch: job lost, destination state unchanged
514 inrow = inspace;
515 if size(outspace,2) > size(inrow,2)
516 inrow = [zeros(1,size(outspace,2)-size(inrow,2)), inrow];
517 end
518 outspace = [outspace; inrow];
519 outrate = [outrate; -1];
520 outprob = [outprob; balkProb];
521 end
522 end
523 if isSimulation
524 if size(outprob,1) > 1
525 cum_prob = cumsum(outprob) / sum(outprob);
526 firing_ctr = 1 + max([0,find( rand > cum_prob' )]); % select action
527 outspace = outspace(firing_ctr,:);
528 outrate = -1;
529 outprob = 1;
530 end
531 end
532 case EventType.DEP
533 % Marked (MMAP) source class: the shared modulating chain lives in the
534 % carrier's phase block (mark index 1); this class's departures fire
535 % from there using its per-mark matrix D1k (M3A cell index 2+mark).
536 markofclass = -1;
537 phclass = class;
538 if sn.sched(ist) == SchedStrategy.EXT && isfield(sn,'markidx') ...
539 && ~isempty(sn.markidx) && ist <= size(sn.markidx,1) ...
540 && sn.markidx(ist,class) > 0
541 markofclass = sn.markidx(ist,class);
542 phclass = find(sn.markidx(ist,:) == 1, 1);
543 end
544 if any(any(space_srv(:,(Ks(phclass)+1):(Ks(phclass)+K(phclass))))) % something is busy
545 if hasOnlyExp && (sn.sched(ist) == SchedStrategy.PS || sn.sched(ist) == SchedStrategy.DPS || sn.sched(ist) == SchedStrategy.GPS || sn.sched(ist) == SchedStrategy.INF || sn.sched(ist) == SchedStrategy.PSPRIO || sn.sched(ist) == SchedStrategy.DPSPRIO || sn.sched(ist) == SchedStrategy.GPSPRIO || sn.sched(ist) == SchedStrategy.LPS)
546 nir = space_srv;
547 ni = sum(nir,2);
548 sir = nir;
549 kir = sir;
550 else
551 [ni,nir,sir,kir] = State.toMarginal(sn,ind,inspace,K,Ks,space_buf,space_srv,space_var);
552 end
553 switch sn.routing(ind,class)
554 case RoutingStrategy.RROBIN
555 idx = find(space_var(sum(sn.nvars(ind,1:(R+class)))) == sn.nodeparam{ind}{class}.outlinks);
556 if idx < length(sn.nodeparam{ind}{class}.outlinks)
557 space_var(sum(sn.nvars(ind,1:(R+class)))) = sn.nodeparam{ind}{class}.outlinks(idx+1);
558 else
559 space_var(sum(sn.nvars(ind,1:(R+class)))) = sn.nodeparam{ind}{class}.outlinks(1);
560 end
561 case RoutingStrategy.WRROBIN
562 % WRR slot holds a POSITION in weighted_outlinks; advance it
563 % cyclically (mirrors afterEventRouter and sub_wrr). Without
564 % this advance the position stays fixed and only the initial
565 % destination is ever selected.
566 slot = sum(sn.nvars(ind,1:(R+class)));
567 if isfield(sn.nodeparam{ind}{class}, 'weighted_outlinks') ...
568 && ~isempty(sn.nodeparam{ind}{class}.weighted_outlinks)
569 cycle_len = length(sn.nodeparam{ind}{class}.weighted_outlinks);
570 else
571 cycle_len = length(sn.nodeparam{ind}{class}.outlinks);
572 end
573 pos = space_var(slot);
574 if pos < 1 || pos >= cycle_len
575 space_var(slot) = 1;
576 else
577 space_var(slot) = pos + 1;
578 end
579 end
580 if sir(phclass)>0 % is a job of class is in service
581 outprob = [];
582 for k=1:K(phclass)
583 space_srv = inspace(:,(end-sum(K)-V+1):(end-V)); % server state
584 space_buf = inspace(:,1:(end-sum(K)-V)); % buffer state
585 rate = zeros(size(space_srv,1),1);
586 en = space_srv(:,Ks(phclass)+k) > 0;
587 if any(en)
588 switch sn.sched(ist)
589 case SchedStrategy.EXT % source, can produce an arrival from phase-k as long as it is from an open class
590 if isinf(sn.njobs(class))
591 if markofclass > 0
592 % per-mark arrival matrix over the shared chain
593 D1_srv = proc{ist}{class}{2+markofclass};
594 else
595 D1_srv = proc{ist}{class}{2};
596 end
597 for kentry = 1:K(phclass)
598 arv_rate = D1_srv(k, kentry);
599 if arv_rate <= 0
600 continue
601 end
602 space_srv = inspace(:,(end-sum(K)-V+1):(end-V));
603 space_srv(en,Ks(phclass)+k) = space_srv(en,Ks(phclass)+k) - 1;
604 space_srv(en,Ks(phclass)+kentry) = space_srv(en,Ks(phclass)+kentry) + 1;
605 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
606 if isinf(ni)
607 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*arv_rate*ones(size(inspace(en,:),1),1)];
608 else
609 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*arv_rate*ones(size(inspace(en,:),1),1)];
610 end
611 outprob = [outprob; ones(size(space_buf(en,:),1),1)];
612 end
613 end
614 case SchedStrategy.INF % move first job in service
615 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
616 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(en,class,k); % assume active
617 % if state is unchanged, still add with rate 0
618 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
619 if isinf(ni) % hit limited load-dependence
620 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
621 else
622 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
623 end
624 outprob = [outprob; ones(size(rate(en,:),1),1)];
625 case {SchedStrategy.PS, SchedStrategy.LPS} % move first job in service
626 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
627 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*(kir(en,class,k)./ni(en)).*min(ni(en),S(ist)); % assume active
628 % if state is unchanged, still add with rate 0
629 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
630 if isinf(ni) % hit limited load-dependence
631 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
632 else
633 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
634 end
635 outprob = [outprob; ones(size(rate(en,:),1),1)];
636 % end
637 case SchedStrategy.PSPRIO
638 % unclear if LD scaling should be with
639 % ni or with niprio, for now left as ni
640 % for consistency with HOL multiserver
641 if all(ni(en) <= S(ist))
642 % n <= c: all jobs get service, priority doesn't matter
643 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
644 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*(kir(en,class,k)./ni(en)).*min(ni(en),S(ist)); % assume active
645 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
646 if isinf(ni) % hit limited load-dependence
647 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
648 else
649 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
650 end
651 outprob = [outprob; ones(size(rate(en,:),1),1)];
652 elseif sn.classprio(class) == min(sn.classprio(nir>0)) % if this class is in the most urgent priority group (lower value = higher priority in LINE)
653 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
654 niprio(en) = sum(nir(sn.classprio==sn.classprio(class))); % jobs at the same priority class
655 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*(kir(en,class,k)./niprio(en)).*min(niprio(en),S(ist)); % assume active
656 % if state is unchanged, still add with rate 0
657 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
658 if isinf(ni) % hit limited load-dependence
659 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
660 else
661 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(niprio(en),lldlimit)).*rate(en,:)];
662 end
663 outprob = [outprob; ones(size(rate(en,:),1),1)];
664 else % n > c and not highest priority: set rate to zero
665 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
666 outrate = [outrate; zeros(size(rate(en,:),1),1)];
667 outprob = [outprob; ones(size(rate(en,:),1),1)];
668 end
669 case SchedStrategy.DPS
670 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
671 if S(ist) > 1
672 line_error(mfilename,'Multi-server DPS stations are not supported yet.');
673 end
674 % in GPS, the scheduling parameter are the weights
675 w_i = sn.schedparam(ist,:);
676 w_i = w_i / sum(w_i);
677 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nir(class))*w_i(class)*nir(class)./(sum(repmat(w_i,sum(en),1)*nir',2));
678 % if state is unchanged, still add with rate 0
679 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
680 if isinf(ni) % hit limited load-dependence
681 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
682 else
683 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
684 end
685 outprob = [outprob; ones(size(rate(en,:),1),1)];
686 case SchedStrategy.DPSPRIO
687 if all(ni(en) <= S(ist))
688 % n <= c: all jobs get service, behave like regular DPS
689 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
690 if S(ist) > 1
691 line_error(mfilename,'Multi-server DPS stations are not supported yet.');
692 end
693 w_i = sn.schedparam(ist,:);
694 w_i = w_i / sum(w_i);
695 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nir(class))*w_i(class)*nir(class)./(sum(repmat(w_i,sum(en),1)*nir',2));
696 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
697 if isinf(ni) % hit limited load-dependence
698 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
699 else
700 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
701 end
702 outprob = [outprob; ones(size(rate(en,:),1),1)];
703 elseif sn.classprio(class) == min(sn.classprio(nir>0)) % if this class is in the most urgent priority group (lower value = higher priority in LINE)
704 nirprio = nir;
705 nirprio(sn.classprio~=sn.classprio(class)) = 0; % ignore jobs of lower priority
706 niprio = sum(nirprio);
707 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
708 if S(ist) > 1
709 line_error(mfilename,'Multi-server DPS stations are not supported yet.');
710 end
711 % in GPS, the scheduling parameter are the weights
712 w_i = sn.schedparam(ist,:);
713 w_i = w_i / sum(w_i);
714 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nirprio(class))*w_i(class)*nirprio(class)./(sum(repmat(w_i,sum(en),1)*nirprio',2));
715 % if state is unchanged, still add with rate 0
716 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
717 if isinf(ni) % hit limited load-dependence
718 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nirprio,en,class).*lldscaling(ist,end).*rate(en,:)];
719 else
720 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nirprio,en,class).*lldscaling(ist,min(niprio(en),lldlimit)).*rate(en,:)];
721 end
722 outprob = [outprob; ones(size(rate(en,:),1),1)];
723 else % n > c and not most urgent priority: set rate to zero
724 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
725 outrate = [outrate; zeros(size(rate(en,:),1),1)];
726 outprob = [outprob; ones(size(rate(en,:),1),1)];
727 end
728 case SchedStrategy.GPS
729 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
730 if S(ist) > 1
731 line_error(mfilename,'Multi-server GPS stations are not supported yet.');
732 end
733 % in GPS, the scheduling parameter are the weights
734 w_i = sn.schedparam(ist,:);
735 w_i = w_i / sum(w_i);
736 cir = min(nir,ones(size(nir)));
737 rate = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nir(class))*w_i(class)/(w_i*cir(:)); % assume active
738 % if state is unchanged, still add with rate 0
739 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
740 if isinf(ni) % hit limited load-dependence
741 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
742 else
743 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
744 end
745 outprob = [outprob; ones(size(rate(en,:),1),1)];
746 case SchedStrategy.GPSPRIO
747 if all(ni(en) <= S(ist))
748 % n <= c: all jobs get service, behave like regular GPS
749 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
750 if S(ist) > 1
751 line_error(mfilename,'Multi-server GPS stations are not supported yet.');
752 end
753 w_i = sn.schedparam(ist,:);
754 w_i = w_i / sum(w_i);
755 cir = min(nir,ones(size(nir)));
756 rate = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nir(class))*w_i(class)/(w_i*cir(:)); % assume active
757 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
758 if isinf(ni) % hit limited load-dependence
759 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
760 else
761 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
762 end
763 outprob = [outprob; ones(size(rate(en,:),1),1)];
764 elseif sn.classprio(class) == min(sn.classprio(nir>0)) % if this class is in the most urgent priority group (lower value = higher priority in LINE)
765 nirprio = nir;
766 nirprio(sn.classprio~=sn.classprio(class)) = 0; % ignore jobs of lower priority
767 niprio = sum(nirprio);
768 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
769 if S(ist) > 1
770 line_error(mfilename,'Multi-server DPS stations are not supported yet.');
771 end
772 % in GPS, the scheduling parameter are the weights
773 w_i = sn.schedparam(ist,:);
774 w_i = w_i / sum(w_i);
775 cir = min(nirprio,ones(size(nirprio)));
776 rate = mu{ist}{class}(k)*(phi{ist}{class}(k))*(kir(en,class,k)/nirprio(class))*w_i(class)/(w_i*cir(:)); % assume active
777 % if state is unchanged, still add with rate 0
778 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
779 if isinf(ni) % hit limited load-dependence
780 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nirprio,en,class).*lldscaling(ist,end).*rate(en,:)];
781 else
782 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nirprio,en,class).*lldscaling(ist,min(niprio(en),lldlimit)).*rate(en,:)];
783 end
784 outprob = [outprob; ones(size(rate(en,:),1),1)];
785 else % n > c and not most urgent priority: set rate to zero
786 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
787 outrate = [outrate; zeros(size(rate(en,:),1),1)];
788 outprob = [outprob; ones(size(rate(en,:),1),1)];
789 end
790 case SchedStrategy.FCFS % move first job in service
791 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
792 en_wbuf = en & ni>S(ist); %states with jobs in buffer
793 if noPromote || isRetrialStation % immediate feedback / retrial orbit: hold server, do not promote a waiting job
794 en_wbuf(:) = false;
795 end
796 for kdest=1:K(class) % new phase
797 space_buf_kd = space_buf;
798 space_var_kd = space_var;
799 if ismkvmodclass(class)
800 space_var_kd(en,sum(sn.nvars(ind,1:class))) = kdest;
801 end
802 rate_kd = rate;
803 rate_kd(en) = proc{ist}{class}{2}(k,kdest).*kir(en,class,k); % assume active
804 % first process states without jobs in buffer
805 en_wobuf = ~en_wbuf;
806 if any(en_wobuf) %any state without jobs in buffer
807 outspace = [outspace; space_buf_kd(en_wobuf,:), space_srv(en_wobuf,:), space_var_kd(en_wobuf,:)];
808 if isinf(ni) % hit limited load-dependence
809 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate_kd(en_wobuf,:)];
810 else
811 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_kd(en_wobuf,:)];
812 end
813 end
814 % now process states with jobs in buffer
815 outprob = [outprob; ones(size(rate_kd(en_wobuf,:),1),1)];
816 if any(en_wbuf) %any state with jobs in buffer
817 % get class of job at head
818 start_svc_class = space_buf_kd(en_wbuf,end);
819 if start_svc_class > 0 % redunant if?
820 % update input buffer
821 space_buf_kd(en_wbuf,:) = [zeros(sum(en_wbuf),1),space_buf_kd(en_wbuf,1:end-1)];
822 % probability vector for the next job of starting in phase kentry
823 if ismkvmodclass(start_svc_class) % if markov-modulated
824 if start_svc_class==class % if successive service from the same class
825 kentry_range = kdest; % new job enters in phase left by departing job
826 else % resume phase from local variables
827 kentry_range = space_var_kd(en,sum(sn.nvars(ind,1:start_svc_class)));
828 end
829 pentry_svc_class = 0*pie{ist}{start_svc_class};
830 pentry_svc_class(kentry_range) = 1.0;
831 else % if i.i.d.
832 pentry_svc_class = pie{ist}{start_svc_class};
833 kentry_range = 1:K(start_svc_class);
834 end
835 for kentry = kentry_range
836 space_srv(en_wbuf,Ks(start_svc_class)+kentry) = space_srv(en_wbuf,Ks(start_svc_class)+kentry) + 1;
837 outspace = [outspace; space_buf_kd(en,:), space_srv(en,:), space_var_kd(en,:)];
838 rate_k = rate_kd;
839 rate_k(en_wbuf,:) = rate_kd(en_wbuf,:)*pentry_svc_class(kentry);
840 if isinf(ni) % use limited load-dependence at the latest user-provided level
841 newrate = State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:);
842 else
843 newrate = State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:);
844 end
845 outrate = [outrate; newrate];
846 % Zero the probability of the branches added *by this kentry* when
847 % their rate is zero. The mask must index newrate, not the whole
848 % accumulated outrate: outrate grows by one entry per kentry while
849 % outprob_cur has one entry per enabled state, so masking on outrate
850 % silently GREW outprob_cur (MATLAB expands on out-of-bound
851 % assignment) once any earlier branch had rate 0 -- which happens
852 % whenever pentry_svc_class has zeros, i.e. a PH whose entry vector
853 % pie does not reach every phase. That misaligned outprob against
854 % outspace/outrate, so the sampled branch read a bogus (often 0)
855 % probability and depRatesSamples under-counted departures.
856 outprob_cur = ones(size(rate_kd(en,:),1),1);
857 outprob_cur(newrate==0.0) = 0;
858 outprob = [outprob; outprob_cur(:)];
859 space_srv(en_wbuf,Ks(start_svc_class)+kentry) = space_srv(en_wbuf,Ks(start_svc_class)+kentry) - 1;
860 end
861 end
862 end
863 end
864 % if state is unchanged, still add with rate 0
865 case SchedStrategy.HOL % FCFS priority
866 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
867 en_wbuf = en & ni>S(ist); %states with jobs in buffer
868 if noPromote || isRetrialStation % immediate feedback / retrial orbit: hold server, do not promote a waiting job
869 en_wbuf(:) = false;
870 end
871 en_wobuf = ~en_wbuf;
872 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
873 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
874 space_buf_groupg = arrayfun(@(x) priogroup(1+x), space_buf);
875 start_classprio = min(space_buf_groupg(en_wbuf,:),[],2); % min finds highest priority
876 isrowmax = space_buf_groupg == repmat(start_classprio, 1, size(space_buf_groupg,2));
877 [~,rightmostMaxPosFlipped]=max(fliplr(isrowmax),[],2);
878 rightmostMaxPos = size(isrowmax,2) - rightmostMaxPosFlipped + 1;
879 start_svc_class = space_buf(en_wbuf, rightmostMaxPos);
880 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
881 if isinf(ni) % hit limited load-dependence
882 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
883 else
884 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
885 end
886 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
887 if start_svc_class > 0
888 pentry_svc_class = pie{ist}{start_svc_class};
889 for kentry = 1:K(start_svc_class)
890 space_srv_k = space_srv;
891 space_buf_k = space_buf;
892 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
893 for j=find(en_wbuf)'
894 space_buf_k(j,:) = [0, space_buf_k(j,1:rightmostMaxPos(j)-1), space_buf_k(j,(rightmostMaxPos(j)+1):end)];
895 end
896 % if state is unchanged, still add with rate 0
897 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
898 rate_k = rate;
899 rate_k(en_wbuf,:) = rate(en_wbuf,:) * pentry_svc_class(kentry);
900 if isinf(ni) % hit limited load-dependence
901 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate_k(en_wbuf,:)];
902 else
903 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en_wbuf,:)];
904 end
905 outprob = [outprob; ones(size(rate_k(en_wbuf,:),1),1)];
906 end
907 end
908 case SchedStrategy.LCFSPRIO % LCFS priority - like HOL but LCFS order within priority groups
909 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
910 en_wbuf = en & ni>S(ist); %states with jobs in buffer
911 if noPromote || isRetrialStation % immediate feedback / retrial orbit: hold server, do not promote a waiting job
912 en_wbuf(:) = false;
913 end
914 en_wobuf = ~en_wbuf;
915 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
916 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
917 space_buf_groupg = arrayfun(@(x) priogroup(1+x), space_buf);
918 start_classprio = min(space_buf_groupg(en_wbuf,:),[],2); % min finds highest priority
919 isrowmax = space_buf_groupg == repmat(start_classprio, 1, size(space_buf_groupg,2));
920 % LCFS: Find leftmost (first) position instead of rightmost for LCFS order
921 [~,leftmostMaxPos]=max(isrowmax,[],2);
922 start_svc_class = space_buf(en_wbuf, leftmostMaxPos);
923 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
924 if isinf(ni) % hit limited load-dependence
925 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
926 else
927 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
928 end
929 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
930 if start_svc_class > 0
931 pentry_svc_class = pie{ist}{start_svc_class};
932 for kentry = 1:K(start_svc_class)
933 space_srv_k = space_srv;
934 space_buf_k = space_buf;
935 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
936 for j=find(en_wbuf)'
937 % LCFS: Remove from leftmost position instead of rightmost
938 space_buf_k(j,:) = [0, space_buf_k(j,1:leftmostMaxPos(j)-1), space_buf_k(j,(leftmostMaxPos(j)+1):end)];
939 end
940 % if state is unchanged, still add with rate 0
941 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
942 rate_k = rate;
943 rate_k(en_wbuf,:) = rate_k(en_wbuf,:)*pentry_svc_class(kentry);
944 if isinf(ni) % hit limited load-dependence
945 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate_k(en_wbuf,:)];
946 else
947 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en_wbuf,:)];
948 end
949 outprob = [outprob; ones(size(rate_k(en_wbuf,:),1),1)];
950 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) - 1;
951 end
952 end
953 case SchedStrategy.LCFS % move last job in service
954 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
955 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
956 en_wbuf = en & ni>S(ist); %states with jobs in buffer
957 if noPromote || isRetrialStation % immediate feedback / retrial orbit: hold server, do not promote a waiting job
958 en_wbuf(:) = false;
959 end
960 % Plain LCFS is not priority-aware: it always promotes the most
961 % recent arrival. Priorities are honored by SchedStrategy.LCFSPRIO,
962 % which has its own case below ("LCFS order within priority
963 % groups"), exactly as FCFS relates to FCFSPRIO. Branching here on
964 % ~all(classprio == classprio(1)) silently turned every LCFS
965 % station with distinct class priorities into an LCFSPRIO one,
966 % contradicting the "Priorities will be ignored" warning raised by
967 % refreshStruct and, for three or more classes, yielding an
968 % all-zero queue length.
969 [~, colfirstnnz] = max( space_buf(en_wbuf,:) ~=0, [], 2 ); % find first nnz column
970 start_svc_class = space_buf(en_wbuf,colfirstnnz); % job entering service
971 space_buf(en_wbuf,colfirstnnz)=0;
972 if isempty(start_svc_class)
973 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
974 if isinf(ni) % hit limited load-dependence
975 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
976 else
977 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
978 end
979 outprob = [outprob; ones(size(rate(en,:),1),1)];
980 if isSimulation && nargin>=7 && isobject(eventCache)
981 eventCache(key) = {outprob, outspace,outrate};
982 end
983 return
984 end
985 for kentry = 1:K(start_svc_class)
986 pentry_svc_class = pie{ist}{start_svc_class};
987 space_srv(en_wbuf,Ks(start_svc_class)+kentry) = space_srv(en_wbuf,Ks(start_svc_class)+kentry) + 1;
988 % if state is unchanged, still add with rate 0
989 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
990 rate_k = rate;
991 rate_k(en_wbuf,:) = rate(en_wbuf,:)*pentry_svc_class(kentry);
992 if isinf(ni) % hit limited load-dependence
993 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:)];
994 else
995 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:)];
996 end
997 outprob = [outprob; ones(size(rate(en,:),1),1)];
998 space_srv(en_wbuf,Ks(start_svc_class)+kentry) = space_srv(en_wbuf,Ks(start_svc_class)+kentry) - 1;
999 end
1000 case SchedStrategy.LCFSPR % move last job in service (preempt-resume)
1001 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1002 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1003 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1004 % Plain LCFSPR resumes the most recently preempted job. It is not
1005 % priority-aware: LCFSPRPRIO is, and it carries its own handling.
1006 % Selecting by priority whenever the class priorities happened to
1007 % differ made this case answer for a policy the user never declared
1008 % (see the arrival path above for the same rule).
1009 [~, colfirstnnz] = max( space_buf(en_wbuf,:) ~=0, [], 2 ); % find first nnz column
1010 start_svc_class = space_buf(en_wbuf,colfirstnnz); % job entering service
1011 kentry = space_buf(en_wbuf,colfirstnnz+1); % entry phase of job resuming service
1012 space_buf(en_wbuf,colfirstnnz)=0;% zero popped job
1013 space_buf(en_wbuf,colfirstnnz+1)=0; % zero popped phase
1014 if isempty(start_svc_class)
1015 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1016 if isinf(ni) % hit limited load-dependence
1017 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1018 else
1019 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1020 end
1021 outprob = [outprob; ones(size(rate(en,:),1),1)];
1022 if isSimulation && nargin>=7 && isobject(eventCache)
1023 eventCache(key) = {outprob, outspace,outrate};
1024 end
1025 return
1026 end
1027 space_srv(en_wbuf,Ks(start_svc_class)+kentry) = space_srv(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1028 % if state is unchanged, still add with rate 0
1029 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1030 if isinf(ni) % hit limited load-dependence
1031 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1032 else
1033 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1034 end
1035 outprob = [outprob; ones(size(rate(en,:),1),1)];
1036 case SchedStrategy.LCFSPI % move last job in service (preempt-independent)
1037 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1038 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1039 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1040 [~, colfirstnnz] = max( space_buf(en_wbuf,:) ~=0, [], 2 ); % find first nnz column
1041 start_svc_class = space_buf(en_wbuf,colfirstnnz); % job entering service
1042 space_buf(en_wbuf,colfirstnnz)=0;% zero popped job
1043 space_buf(en_wbuf,colfirstnnz+1)=0; % zero popped phase (ignored for LCFSPI)
1044 if isempty(start_svc_class)
1045 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1046 if isinf(ni) % hit limited load-dependence
1047 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1048 else
1049 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1050 end
1051 outprob = [outprob; ones(size(rate(en,:),1),1)];
1052 if isSimulation && nargin>=7 && isobject(eventCache)
1053 eventCache(key) = {outprob, outspace,outrate};
1054 end
1055 return
1056 end
1057 % For LCFSPI, jobs restart from pie distribution instead of stored phase
1058 pentry_svc_class = pie{ist}{start_svc_class};
1059 for kentry = 1:K(start_svc_class)
1060 space_srv_k = space_srv;
1061 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1062 % if state is unchanged, still add with rate 0
1063 outspace = [outspace; space_buf(en,:), space_srv_k(en,:), space_var(en,:)];
1064 rate_k = rate;
1065 rate_k(en_wbuf,:) = rate_k(en_wbuf,:) * pentry_svc_class(kentry);
1066 if isinf(ni) % hit limited load-dependence
1067 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:)];
1068 else
1069 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:)];
1070 end
1071 outprob = [outprob; ones(size(rate_k(en,:),1),1)];
1072 end
1073 case SchedStrategy.FCFSPR % FCFS preempt-resume (no priority)
1074 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1075 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1076 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1077 % FCFS: Find rightmost (last) non-zero position (longest waiting job)
1078 [~, colLastNnz] = max(fliplr(space_buf(en_wbuf,:) ~= 0), [], 2);
1079 colLastNnz = size(space_buf,2) - colLastNnz; % convert to actual position (odd index for class)
1080 start_svc_class = space_buf(en_wbuf, colLastNnz); % job entering service
1081 kentry = space_buf(en_wbuf, colLastNnz+1); % entry phase of job resuming service
1082 space_buf(en_wbuf, colLastNnz) = 0; % zero popped job
1083 space_buf(en_wbuf, colLastNnz+1) = 0; % zero popped phase
1084 if isempty(start_svc_class)
1085 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1086 if isinf(ni)
1087 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1088 else
1089 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1090 end
1091 outprob = [outprob; ones(size(rate(en,:),1),1)];
1092 if isSimulation && nargin>=7 && isobject(eventCache)
1093 eventCache(key) = {outprob, outspace, outrate};
1094 end
1095 return
1096 end
1097 space_srv(en_wbuf, Ks(start_svc_class)+kentry) = space_srv(en_wbuf, Ks(start_svc_class)+kentry) + 1;
1098 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1099 if isinf(ni)
1100 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1101 else
1102 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1103 end
1104 outprob = [outprob; ones(size(rate(en,:),1),1)];
1105 case SchedStrategy.FCFSPI % FCFS preempt-independent (no priority)
1106 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1107 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1108 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1109 % FCFS: Find rightmost (last) non-zero position (longest waiting job)
1110 [~, colLastNnz] = max(fliplr(space_buf(en_wbuf,:) ~= 0), [], 2);
1111 colLastNnz = size(space_buf,2) - colLastNnz; % convert to actual position (odd index for class)
1112 start_svc_class = space_buf(en_wbuf, colLastNnz); % job entering service
1113 space_buf(en_wbuf, colLastNnz) = 0; % zero popped job
1114 space_buf(en_wbuf, colLastNnz+1) = 0; % zero popped phase (ignored for FCFSPI)
1115 if isempty(start_svc_class)
1116 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1117 if isinf(ni)
1118 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate(en,:)];
1119 else
1120 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en,:)];
1121 end
1122 outprob = [outprob; ones(size(rate(en,:),1),1)];
1123 if isSimulation && nargin>=7 && isobject(eventCache)
1124 eventCache(key) = {outprob, outspace, outrate};
1125 end
1126 return
1127 end
1128 % For FCFSPI, jobs restart from pie distribution instead of stored phase
1129 pentry_svc_class = pie{ist}{start_svc_class};
1130 for kentry = 1:K(start_svc_class)
1131 space_srv_k = space_srv;
1132 space_srv_k(en_wbuf, Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf, Ks(start_svc_class)+kentry) + 1;
1133 outspace = [outspace; space_buf(en,:), space_srv_k(en,:), space_var(en,:)];
1134 rate_k = rate;
1135 rate_k(en_wbuf,:) = rate_k(en_wbuf,:) * pentry_svc_class(kentry);
1136 if isinf(ni)
1137 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:)];
1138 else
1139 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:)];
1140 end
1141 outprob = [outprob; ones(size(rate_k(en,:),1),1)];
1142 end
1143 case SchedStrategy.FCFSPRPRIO % FCFS preempt-resume with priority groups
1144 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1145 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1146 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1147 en_wobuf = ~en_wbuf;
1148 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
1149 % Buffer stores [class,phase,class,phase,...] pairs;
1150 % only inspect class columns (odd: 1,3,5,...) for priority
1151 class_cols = 1:2:size(space_buf,2);
1152 space_buf_class = space_buf(:, class_cols);
1153 space_buf_class_groupg = arrayfun(@(x) priogroup(1+x), space_buf_class);
1154 start_classprio = min(space_buf_class_groupg(en_wbuf,:),[],2); % min finds highest priority
1155 isrowmax = space_buf_class_groupg == repmat(start_classprio, 1, size(space_buf_class_groupg,2));
1156 % FCFS: Find rightmost (last) class position for highest priority class (longest waiting)
1157 [~,rightmostClassPos]=max(fliplr(isrowmax),[],2);
1158 rightmostClassPos = size(space_buf_class_groupg,2) - rightmostClassPos + 1;
1159 rightmostMaxPos = 2*rightmostClassPos - 1; % convert to buffer column index
1160 start_svc_class = space_buf(en_wbuf, rightmostMaxPos); % job entering service
1161 kentry = space_buf(en_wbuf, rightmostMaxPos+1); % entry phase of job resuming service (preempt-resume)
1162
1163 % Handle states without buffer jobs
1164 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
1165 if isinf(ni)
1166 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
1167 else
1168 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
1169 end
1170 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
1171
1172 % Handle states with buffer jobs
1173 if any(en_wbuf) && start_svc_class > 0
1174 space_srv_k = space_srv;
1175 space_buf_k = space_buf;
1176 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1177 for j=find(en_wbuf)'
1178 % Remove both class and phase from rightmost position (preempt-resume)
1179 space_buf_k(j,:) = [0, space_buf_k(j,1:rightmostMaxPos(j)-1), space_buf_k(j,(rightmostMaxPos(j)+2):end), 0];
1180 end
1181 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
1182 if isinf(ni)
1183 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate(en_wbuf,:)];
1184 else
1185 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wbuf,:)];
1186 end
1187 outprob = [outprob; ones(size(rate(en_wbuf,:),1),1)];
1188 end
1189 case SchedStrategy.LCFSPRPRIO % LCFS preempt-resume with priority groups
1190 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1191 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1192 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1193 en_wobuf = ~en_wbuf;
1194 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
1195 % Buffer stores [class,phase,class,phase,...] pairs;
1196 % only inspect class columns (odd: 1,3,5,...) for priority
1197 class_cols = 1:2:size(space_buf,2);
1198 space_buf_class = space_buf(:, class_cols);
1199 space_buf_class_groupg = arrayfun(@(x) priogroup(1+x), space_buf_class);
1200 start_classprio = min(space_buf_class_groupg(en_wbuf,:),[],2); % min finds highest priority
1201 isrowmax = space_buf_class_groupg == repmat(start_classprio, 1, size(space_buf_class_groupg,2));
1202 % LCFS: Find leftmost (first) class position for highest priority class (most recently preempted)
1203 [~,leftmostClassPos]=max(isrowmax,[],2);
1204 leftmostMaxPos = 2*leftmostClassPos - 1; % convert to buffer column index
1205 start_svc_class = space_buf(en_wbuf, leftmostMaxPos); % job entering service
1206 kentry = space_buf(en_wbuf, leftmostMaxPos+1); % entry phase of job resuming service (preempt-resume)
1207
1208 % Handle states without buffer jobs
1209 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
1210 if isinf(ni)
1211 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
1212 else
1213 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
1214 end
1215 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
1216
1217 % Handle states with buffer jobs
1218 if any(en_wbuf) && start_svc_class > 0
1219 space_srv_k = space_srv;
1220 space_buf_k = space_buf;
1221 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1222 for j=find(en_wbuf)'
1223 % Remove both class and phase from leftmost position (preempt-resume)
1224 % The (class,phase) pair buffer is RIGHT-aligned: occupied pairs sit at the
1225 % right, empty pairs pad the left. Priority can promote a pair from the
1226 % middle, so closing the hole must pad a whole empty PAIR on the left --
1227 % as State.afterEventStationSignal's dropWaiting does. Padding one slot
1228 % left and one right instead left a trailing empty slot, e.g.
1229 % [2 1 3 1] -> [0 3 1 0], a layout fromMarginal never enumerates: the
1230 % successor was unreachable, which is what made the generator reducible.
1231 space_buf_k(j,:) = [0, 0, space_buf_k(j,1:leftmostMaxPos(j)-1), space_buf_k(j,(leftmostMaxPos(j)+2):end)];
1232 end
1233 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
1234 if isinf(ni)
1235 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate(en_wbuf,:)];
1236 else
1237 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wbuf,:)];
1238 end
1239 outprob = [outprob; ones(size(rate(en_wbuf,:),1),1)];
1240 end
1241 case SchedStrategy.FCFSPIPRIO % FCFS preempt-independent with priority groups
1242 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1243 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1244 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1245 en_wobuf = ~en_wbuf;
1246 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
1247 space_buf_groupg = arrayfun(@(x) priogroup(1+x), space_buf);
1248 start_classprio = min(space_buf_groupg(en_wbuf,:),[],2); % min finds highest priority
1249 isrowmax = space_buf_groupg == repmat(start_classprio, 1, size(space_buf_groupg,2));
1250 % FCFS: Find rightmost (last) position for highest priority class
1251 [~,rightmostMaxPos]=max(fliplr(isrowmax),[],2);
1252 rightmostMaxPos = size(space_buf_groupg,2) - rightmostMaxPos + 1;
1253 start_svc_class = space_buf(en_wbuf, rightmostMaxPos); % job entering service
1254
1255 % Handle states without buffer jobs
1256 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
1257 if isinf(ni)
1258 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
1259 else
1260 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
1261 end
1262 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
1263
1264 % Handle states with buffer jobs
1265 if any(en_wbuf) && start_svc_class > 0
1266 % For FCFSPIPRIO, jobs restart from pie distribution instead of stored phase
1267 pentry_svc_class = pie{ist}{start_svc_class};
1268 for kentry = 1:K(start_svc_class)
1269 space_srv_k = space_srv;
1270 space_buf_k = space_buf;
1271 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1272 for j=find(en_wbuf)'
1273 % Remove both class and phase from rightmost position (preempt-independent ignores stored phase)
1274 space_buf_k(j,:) = [0, space_buf_k(j,1:rightmostMaxPos(j)-1), space_buf_k(j,(rightmostMaxPos(j)+2):end), 0];
1275 end
1276 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
1277 rate_k = rate;
1278 rate_k(en_wbuf,:) = rate_k(en_wbuf,:) * pentry_svc_class(kentry);
1279 if isinf(ni)
1280 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate_k(en_wbuf,:)];
1281 else
1282 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en_wbuf,:)];
1283 end
1284 outprob = [outprob; ones(size(rate_k(en_wbuf,:),1),1)];
1285 end
1286 end
1287 case SchedStrategy.LCFSPIPRIO % LCFS preempt-independent with priority groups
1288 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1289 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % assume active
1290 en_wbuf = en & ni>S(ist); %states with jobs in buffer
1291 en_wobuf = ~en_wbuf;
1292 priogroup = [Inf,sn.classprio]; % Inf for empty positions (lower value = higher priority)
1293 space_buf_groupg = arrayfun(@(x) priogroup(1+x), space_buf);
1294 start_classprio = min(space_buf_groupg(en_wbuf,:),[],2); % min finds highest priority
1295 isrowmax = space_buf_groupg == repmat(start_classprio, 1, size(space_buf_groupg,2));
1296 % LCFS: Find leftmost (first) position for highest priority class
1297 [~,leftmostMaxPos]=max(isrowmax,[],2);
1298 start_svc_class = space_buf(en_wbuf, leftmostMaxPos); % job entering service
1299
1300 % Handle states without buffer jobs
1301 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
1302 if isinf(ni)
1303 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
1304 else
1305 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
1306 end
1307 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
1308
1309 % Handle states with buffer jobs
1310 if any(en_wbuf) && start_svc_class > 0
1311 % For LCFSPIPRIO, jobs restart from pie distribution instead of stored phase
1312 pentry_svc_class = pie{ist}{start_svc_class};
1313 for kentry = 1:K(start_svc_class)
1314 space_srv_k = space_srv;
1315 space_buf_k = space_buf;
1316 space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) = space_srv_k(en_wbuf,Ks(start_svc_class)+kentry) + 1;
1317 for j=find(en_wbuf)'
1318 % Remove both class and phase from leftmost position (preempt-independent ignores stored phase)
1319 % The (class,phase) pair buffer is RIGHT-aligned: occupied pairs sit at the
1320 % right, empty pairs pad the left. Priority can promote a pair from the
1321 % middle, so closing the hole must pad a whole empty PAIR on the left --
1322 % as State.afterEventStationSignal's dropWaiting does. Padding one slot
1323 % left and one right instead left a trailing empty slot, e.g.
1324 % [2 1 3 1] -> [0 3 1 0], a layout fromMarginal never enumerates: the
1325 % successor was unreachable, which is what made the generator reducible.
1326 space_buf_k(j,:) = [0, 0, space_buf_k(j,1:leftmostMaxPos(j)-1), space_buf_k(j,(leftmostMaxPos(j)+2):end)];
1327 end
1328 outspace = [outspace; space_buf_k(en_wbuf,:), space_srv_k(en_wbuf,:), space_var(en_wbuf,:)];
1329 rate_k = rate;
1330 rate_k(en_wbuf,:) = rate_k(en_wbuf,:) * pentry_svc_class(kentry);
1331 if isinf(ni)
1332 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate_k(en_wbuf,:)];
1333 else
1334 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en_wbuf,:)];
1335 end
1336 outprob = [outprob; ones(size(rate_k(en_wbuf,:),1),1)];
1337 end
1338 end
1339 case SchedStrategy.SIRO
1340 rate = zeros(size(space_srv,1),1);
1341 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % this is for states not in en_buf
1342 space_srv = inspace(:,(end-sum(K)-V+1):(end-V)); % server state (clear of the local vars)
1343 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1344 % first record departure in states where the buffer is empty
1345 en_wobuf = en & sum(space_buf(en,:),2) == 0;
1346 if noPromote % immediate feedback: hold server, do not promote a waiting job
1347 en_wobuf = en;
1348 end
1349 outspace = [outspace; space_buf(en_wobuf,:), space_srv(en_wobuf,:), space_var(en_wobuf,:)];
1350 if isinf(ni) % hit limited load-dependence
1351 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,end).*rate(en_wobuf,:)];
1352 else
1353 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wobuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate(en_wobuf,:)];
1354 end
1355 outprob = [outprob; ones(size(rate(en_wobuf,:),1),1)];
1356 % let's go now to states where the buffer is non-empty
1357 % (promotion suppressed under immediate feedback: R*(~noPromote)=0)
1358 for r=1:(R*(~noPromote))
1359 rate_r = rate;
1360 space_buf = inspace(:,1:(end-sum(K)-V)); % buffer state (clear of the local vars)
1361 en_wbuf = en & space_buf(en,r) > 0; % states where the buffer is non-empty
1362 space_buf(en_wbuf,r) = space_buf(en_wbuf,r) - 1; % remove from buffer
1363 space_srv_r = space_srv;
1364 pentry_svc_class = pie{ist}{r};
1365 pick_prob = (nir(r)-sir(r)) / (ni-sum(sir));
1366 if pick_prob >= 0
1367 rate_r(en_wbuf,:) = rate_r(en_wbuf,:) * pick_prob;
1368 end
1369 for kentry=1:K(r)
1370 space_srv_r(en_wbuf,Ks(r)+kentry) = space_srv_r(en_wbuf,Ks(r)+kentry) + 1; % bring job in service
1371 outspace = [outspace; space_buf(en_wbuf,:), space_srv_r(en_wbuf,:), space_var(en_wbuf,:)];
1372 rate_k = rate_r;
1373 rate_k(en_wbuf,:) = rate_k(en_wbuf,:) * pentry_svc_class(kentry);
1374 if isinf(ni) % hit limited load-dependence
1375 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,end).*rate_k(en_wbuf,:)];
1376 else
1377 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en_wbuf,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en_wbuf,:)];
1378 end
1379 outprob = [outprob; ones(size(rate(en_wbuf,:),1),1)];
1380 space_srv_r(en_wbuf,Ks(r)+kentry) = space_srv_r(en_wbuf,Ks(r)+kentry) - 1; % bring job in service
1381 end
1382 end
1383 case SchedStrategy.POLLING
1384 % A completion ends the visit unless the
1385 % discipline still allows another job of the
1386 % same class to be taken; when it ends, the
1387 % server walks the cyclic order to wherever the
1388 % next tangible controller state lies. Which of
1389 % the two happens depends on the buffer of each
1390 % state row, so the rows are resolved one by one
1391 % rather than as a single vectorized promotion.
1392 % space_buf/space_srv are re-sliced from inspace by the
1393 % enclosing k-loop; space_var deliberately is NOT, since
1394 % it already carries the RROBIN/WRROBIN pointer advance
1395 % applied above and re-slicing would discard it.
1396 pinfoD = State.pollingInfo(sn, ind);
1397 for rowD = find(en(:))'
1398 rateD = mu{ist}{class}(k)*phi{ist}{class}(k)*kir(rowD,class,k);
1399 if rateD <= 0
1400 continue
1401 end
1402 [~, swkD, ctrD] = State.pollingGet(pinfoD, space_var(rowD,:), class);
1403 if swkD ~= 0
1404 continue % no job can complete while the server is walking
1405 end
1406 bufD = space_buf(rowD,:);
1407 nbufD = bufD(1,1:R);
1408 srvD = space_srv(rowD,:);
1409 srvD(1,Ks(class)+k) = srvD(1,Ks(class)+k) - 1; % record departure
1410 switch pinfoD.ptype
1411 case PollingType.EXHAUSTIVE
1412 ctrnextD = 0;
1413 goonD = nbufD(class) > 0;
1414 case PollingType.GATED
1415 ctrnextD = ctrD - 1; % one of the gated jobs completed
1416 goonD = ctrnextD > 0;
1417 case PollingType.KLIMITED
1418 ctrnextD = ctrD - 1; % one of the K permitted services used
1419 goonD = ctrnextD > 0 && nbufD(class) > 0;
1420 case PollingType.DECREMENTING
1421 ctrnextD = ctrD; % the target level is fixed for the visit
1422 goonD = nbufD(class) > ctrD;
1423 end
1424 if goonD
1425 qD = class; modeD = 1; budgetD = ctrnextD;
1426 else
1427 [qD, modeD, budgetD] = State.pollingNext(pinfoD, class, nbufD, R, false);
1428 end
1429 [rowsD, probsD] = State.pollingLand(pinfoD, qD, modeD, budgetD, ...
1430 bufD, srvD, space_var(rowD,:), K, Ks, pie{ist}, R);
1431 for jD = 1:size(rowsD,1)
1432 outspace = [outspace; rowsD(jD,:)];
1433 if isinf(ni) % hit limited load-dependence
1434 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,rowD,class).*lldscaling(ist,end).*rateD.*probsD(jD)];
1435 else
1436 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,rowD,class).*lldscaling(ist,min(ni(rowD),lldlimit)).*rateD.*probsD(jD)];
1437 end
1438 outprob = [outprob; 1];
1439 end
1440 end
1441 case {SchedStrategy.SEPT,SchedStrategy.LEPT} % move last job in service
1442 rate = zeros(size(space_srv,1),1);
1443 rate(en) = mu{ist}{class}(k)*(phi{ist}{class}(k)).*kir(:,class,k); % this is for states not in en_buf
1444 space_srv = inspace(:,(end-sum(K)-V+1):(end-V)); % server state (clear of the local vars)
1445 space_srv(en,Ks(class)+k) = space_srv(en,Ks(class)+k) - 1; % record departure
1446 space_buf = inspace(:,1:(end-sum(K)-V)); % buffer state (clear of the local vars)
1447 % in SEPT, the scheduling parameter is the priority order of the class means
1448 % en_wbuf: states where the buffer is non-empty
1449 % sept_class: class to pick in service
1450 % sn.schedparam(ist,r) holds the RANK of class r's mean service
1451 % time (ascending for SEPT, descending for LEPT, see
1452 % MNetwork.refreshScheduling). Scanning the buffer columns in
1453 % schedparam order visits the classes as P(1),P(2),..., which
1454 % coincides with rank order only when P is an involution -- true
1455 % of every 2-class model, but false in general, in which case the
1456 % wrong class is promoted. The discipline needs the inverse map
1457 % rank -> class, so invert the permutation first.
1458 [~, classByRank] = sort(sn.schedparam(ist,1:R));
1459 [en_wbuf, first_class_inrow] = max(space_buf(:,classByRank)~=0, [], 2);
1460 sept_class = classByRank(first_class_inrow); % this is different for sept and lept
1461 if noPromote % immediate feedback: hold server, do not promote a waiting job
1462 en_wbuf(:) = false;
1463 end
1464
1465 space_buf(en_wbuf,sept_class) = space_buf(en_wbuf,sept_class) - 1; % remove from buffer
1466 pentry = pie{ist}{sept_class};
1467 for kentry=1:K(sept_class)
1468 space_srv(en_wbuf,Ks(sept_class)+kentry) = space_srv(en_wbuf,Ks(sept_class)+kentry) + 1; % bring job in service
1469 if isSimulation
1470 % break the tie
1471 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1472 rate_k = rate;
1473 rate_k(en,:) = rate_k(en,:) * pentry(kentry);
1474 if isinf(ni) % hit limited load-dependence
1475 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:)];
1476 else
1477 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:)];
1478 end
1479 outprob = [outprob; ones(size(rate(en,:),1),1)];
1480 else
1481 outspace = [outspace; space_buf(en,:), space_srv(en,:), space_var(en,:)];
1482 rate_k = rate;
1483 rate_k(en,:) = rate_k(en,:) * pentry(kentry);
1484 if isinf(ni) % hit limited load-dependence
1485 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,end).*rate_k(en,:)];
1486 else
1487 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,en,class).*lldscaling(ist,min(ni(en),lldlimit)).*rate_k(en,:)];
1488 end
1489 outprob = [outprob; ones(size(rate(en,:),1),1)];
1490 end
1491 space_srv(en_wbuf,Ks(sept_class)+kentry) = space_srv(en_wbuf,Ks(sept_class)+kentry) - 1; % bring job in service
1492 end
1493 otherwise
1494 line_error(mfilename,sprintf('Scheduling strategy %s is not supported.', SchedStrategy.toText(sn.sched(ist))));
1495 end
1496 end
1497 end
1498 if isSimulation
1499 if nargin>=7 && isobject(eventCache)
1500 eventCache(key) = {outprob, outspace,outrate};
1501 end
1502
1503 if size(outspace,1) > 1
1504 tot_rate = sum(outrate);
1505 cum_rate = cumsum(outrate) / tot_rate;
1506 firing_ctr = 1 + max([0,find( rand > cum_rate' )]); % select action
1507 outspace = outspace(firing_ctr,:);
1508 outrate = sum(outrate);
1509 outprob = outprob(firing_ctr,:);
1510 end
1511 end
1512 end
1513 end
1514 case EventType.PHASE
1515 outspace = [];
1516 outrate = [];
1517 outprob = [];
1518 [ni,nir,~,kir] = State.toMarginal(sn,ind,inspace,K,Ks,space_buf,space_srv,space_var);
1519 if nir(class)>0
1520 for k=1:K(class)
1521 en = space_srv(:,Ks(class)+k) > 0;
1522 if any(en)
1523 for kdest=setdiff(1:K(class),k) % new phase
1524 rate = 0;
1525 space_srv_k = space_srv(en,:);
1526 space_buf_k = space_buf(en,:);
1527 space_var_k = space_var(en,:);
1528 if ismkvmodclass(class) && ~isempty(space_var_k)
1529 space_var_k(sum(sn.nvars(ind,1:class))) = kdest;
1530 end
1531 space_srv_k(:,Ks(class)+k) = space_srv_k(:,Ks(class)+k) - 1;
1532 space_srv_k(:,Ks(class)+kdest) = space_srv_k(:,Ks(class)+kdest) + 1;
1533 switch sn.sched(ist)
1534 case SchedStrategy.EXT
1535 rate = proc{ist}{class}{1}(k,kdest); % move next job forward
1536 case SchedStrategy.INF
1537 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k); % assume active
1538 case {SchedStrategy.PS, SchedStrategy.LPS}
1539 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)./ni(:).*min(ni(:),S(ist)); % assume active
1540 case SchedStrategy.PSPRIO
1541 if all(ni <= S(ist)) || sn.classprio(class) == min(sn.classprio(nir>0))
1542 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)./ni(:).*min(ni(:),S(ist)); % assume active
1543 else
1544 rate = 0; % not in most urgent priority group
1545 end
1546 case SchedStrategy.DPSPRIO
1547 if all(ni <= S(ist)) || sn.classprio(class) == min(sn.classprio(nir>0))
1548 w_i = sn.schedparam(ist,:);
1549 w_i = w_i / sum(w_i);
1550 nirprio = nir;
1551 if ~all(ni <= S(ist))
1552 nirprio(sn.classprio~=sn.classprio(class)) = 0;
1553 end
1554 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)*w_i(class)./(sum(repmat(w_i,size(nirprio,1),1)*nirprio',2)); % assume active
1555 else
1556 rate = 0; % not in most urgent priority group
1557 end
1558 case SchedStrategy.GPSPRIO
1559 if all(ni <= S(ist)) || sn.classprio(class) == min(sn.classprio(nir>0))
1560 w_i = sn.schedparam(ist,:);
1561 w_i = w_i / sum(w_i);
1562 nirprio = nir;
1563 if ~all(ni <= S(ist))
1564 nirprio(sn.classprio~=sn.classprio(class)) = 0;
1565 end
1566 cir = min(nirprio,ones(size(nirprio)));
1567 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)/nirprio(class)*w_i(class)/(w_i*cir(:)); % assume active
1568 else
1569 rate = 0; % not in most urgent priority group
1570 end
1571 case SchedStrategy.DPS
1572 if S(ist) > 1
1573 line_error(mfilename,'Multi-server DPS not supported yet');
1574 end
1575 w_i = sn.schedparam(ist,:);
1576 w_i = w_i / sum(w_i);
1577 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)*w_i(class)./(sum(repmat(w_i,size(nir,1),1)*nir',2)); % assume active
1578 case SchedStrategy.GPS
1579 if S(ist) > 1
1580 line_error(mfilename,'Multi-server GPS not supported yet');
1581 end
1582 cir = min(nir,ones(size(nir)));
1583 w_i = sn.schedparam(ist,:); w_i = w_i / sum(w_i);
1584 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k)/nir(class)*w_i(class)/(w_i*cir(:)); % assume active
1585
1586 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.LCFS, SchedStrategy.LCFSPR, SchedStrategy.LCFSPI, SchedStrategy.LCFSPRIO, SchedStrategy.SIRO, SchedStrategy.SEPT, SchedStrategy.LEPT, SchedStrategy.POLLING}
1587 rate = proc{ist}{class}{1}(k,kdest)*kir(:,class,k); % assume active
1588 end
1589 % if the class cannot be served locally,
1590 % then rate = NaN since mu{i,class}=NaN
1591 if isinf(ni) % hit limited load-dependence
1592 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,1:size(rate,1),class).*lldscaling(ist,end).*rate];
1593 else
1594 outrate = [outrate; State.cdclassfactor(cdscaling{ist},nir,1:size(rate,1),class).*lldscaling(ist,min(ni(en),lldlimit)).*rate];
1595 end
1596 outspace = [outspace; space_buf_k, space_srv_k, space_var_k];
1597 outprob = [outprob; ones(size(rate,1),1)];
1598 end
1599 end
1600 end
1601 if isSimulation
1602 if nargin>=7 && isobject(eventCache)
1603 eventCache(key) = {outprob, outspace,outrate};
1604 end
1605
1606 if size(outspace,1) > 1
1607 tot_rate = sum(outrate);
1608 cum_rate = cumsum(outrate) / tot_rate;
1609 firing_ctr = 1 + max([0,find( rand > cum_rate' )]); % select action
1610 outspace = outspace(firing_ctr,:);
1611 outrate = sum(outrate);
1612 outprob = outprob(firing_ctr,:);
1613 end
1614 end
1615 end
1616 case EventType.SWITCH
1617 % Switchover of a polling server walking towards buffer `class`. Unlike
1618 % PHASE, which carries only the internal transitions of a phase-type
1619 % and leaves the absorption to DEP, this event carries both: a
1620 % completed switchover moves no job and so has no departure to attach
1621 % the absorption to (see refreshSync). It is therefore also emitted for
1622 % a single-phase switchover, where it consists of the absorption alone.
1623 outspace = [];
1624 outrate = [];
1625 outprob = [];
1626 pinfoS = State.pollingInfo(sn, ind);
1627 if isempty(pinfoS) || ~pinfoS.hasSw(class)
1628 return
1629 end
1630 for rowS = 1:size(inspace,1)
1631 [posS, swkS] = State.pollingGet(pinfoS, space_var(rowS,:), 0);
1632 if posS ~= class || swkS == 0
1633 continue % the server is not inside the switchover into `class`
1634 end
1635 D0S = pinfoS.swD0{class};
1636 D1S = pinfoS.swD1{class};
1637 % internal transitions of the switchover phase-type
1638 for kdestS = setdiff(1:pinfoS.Ksw(class), swkS)
1639 if D0S(swkS,kdestS) <= 0
1640 continue
1641 end
1642 varS = State.pollingSet(pinfoS, space_var(rowS,:), class, kdestS, 0);
1643 outspace = [outspace; space_buf(rowS,:), space_srv(rowS,:), varS];
1644 outrate = [outrate; D0S(swkS,kdestS)];
1645 outprob = [outprob; 1];
1646 end
1647 % absorption: the server arrives at buffer `class` and either opens
1648 % a visit there or walks on
1649 rateS = sum(D1S(swkS,:));
1650 if rateS <= 0
1651 continue
1652 end
1653 nbufS = space_buf(rowS,1:R);
1654 [qS, modeS, budgetS] = State.pollingNext(pinfoS, class, nbufS, R, true);
1655 [rowsS, probsS] = State.pollingLand(pinfoS, qS, modeS, budgetS, ...
1656 space_buf(rowS,:), space_srv(rowS,:), space_var(rowS,:), K, Ks, pie{ist}, R);
1657 for jS = 1:size(rowsS,1)
1658 % A switchover that completes over an empty buffer starts the
1659 % next leg at once, and when that leg re-enters the same phase
1660 % of the same switchover (the single-buffer case, or any
1661 % memoryless switchover) the landing state is the departure
1662 % state. Such a self-loop is not a transition: emitting it would
1663 % add a spurious rate to the row and inflate the exit rate.
1664 if isequal(rowsS(jS,:), inspace(rowS,:))
1665 continue
1666 end
1667 outspace = [outspace; rowsS(jS,:)];
1668 outrate = [outrate; rateS*probsS(jS)];
1669 outprob = [outprob; 1];
1670 end
1671 end
1672 case EventType.RENEGE
1673 % Exponential-patience reneging: each waiting (queued, not-in-service)
1674 % class-r job abandons the queue at a memoryless rate impatienceMu, so
1675 % the aggregate rate out of this state is (waiting count) * mu. One
1676 % waiting job is removed from the buffer and leaves the system (the
1677 % passive half of the sync is LOCAL). Removing the newest waiting
1678 % class-r job and re-padding a zero keeps the buffer in the canonical
1679 % right-aligned form used by the arrival handler; for memoryless
1680 % patience all waiting jobs are exchangeable, so the choice does not
1681 % affect the marginal distribution.
1682 outspace = [];
1683 outrate = [];
1684 outprob = [];
1685 [~,nir,sir] = State.toMarginal(sn,ind,inspace,K,Ks,space_buf,space_srv,space_var);
1686 waiting_r = nir(class) - sir(class);
1687 if waiting_r > 0
1688 slot = find(space_buf == class, 1, 'first');
1689 if ~isempty(slot)
1690 space_buf_k = space_buf;
1691 space_buf_k(slot) = [];
1692 space_buf_k = [0, space_buf_k];
1693 outspace = [space_buf_k, space_srv, space_var];
1694 outrate = waiting_r * sn.impatienceMu(ist,class);
1695 outprob = 1;
1696 end
1697 end
1698 case EventType.RETRY
1699 % Exponential retrial: an orbiting (buffered) class-r job retries entry
1700 % into the station. The retry succeeds only when a server is free, in
1701 % which case one orbiting job enters service (in an entry phase drawn
1702 % from pie); otherwise the job stays in orbit and the event is a no-op
1703 % (not generated). The aggregate rate is (orbit size) * retrialMu.
1704 outspace = [];
1705 outrate = [];
1706 outprob = [];
1707 [~,nir,sir] = State.toMarginal(sn,ind,inspace,K,Ks,space_buf,space_srv,space_var);
1708 orbit_r = nir(class) - sir(class); % orbiting class-r jobs (held in buffer)
1709 if orbit_r > 0 && sum(space_srv,2) < S(ist)
1710 slot = find(space_buf == class, 1, 'first');
1711 if ~isempty(slot)
1712 space_buf_k = space_buf;
1713 space_buf_k(slot) = [];
1714 space_buf_k = [0, space_buf_k];
1715 pentry = pie{ist}{class};
1716 if all(isnan(pentry))
1717 pentry = ones(size(pentry)) / length(pentry);
1718 end
1719 for kentry = 1:K(class)
1720 if pentry(kentry) <= 0
1721 continue;
1722 end
1723 space_srv_k = space_srv;
1724 space_srv_k(:,Ks(class)+kentry) = space_srv_k(:,Ks(class)+kentry) + 1;
1725 % LINEAR policy: every orbiting job carries its own timer, so
1726 % the aggregate retrial rate scales with the orbit size.
1727 % CONSTANT policy: one controller retries on behalf of the
1728 % whole orbit, so the rate does not depend on the orbit size.
1729 retrialRate = orbit_r * sn.retrialMu(ist,class);
1730 if isfield(sn,'retrialPolicy') && ~isempty(sn.retrialPolicy) ...
1731 && size(sn.retrialPolicy,1) >= ist && size(sn.retrialPolicy,2) >= class ...
1732 && sn.retrialPolicy(ist,class) == RetrialPolicy.CONSTANT
1733 retrialRate = sn.retrialMu(ist,class);
1734 end
1735 outspace = [outspace; space_buf_k, space_srv_k, space_var];
1736 outrate = [outrate; retrialRate * pentry(kentry)];
1737 outprob = [outprob; 1];
1738 end
1739 if isSimulation && size(outspace,1) > 1
1740 cr = cumsum(outrate) / sum(outrate);
1741 fc = 1 + max([0, find(rand > cr')]);
1742 outspace = outspace(fc,:);
1743 outrate = sum(outrate);
1744 outprob = 1;
1745 end
1746 end
1747 end
1748end
1749
1750% True BAS: when the front job is already blocked (completed, held at the server), this
1751% DEP is the *instant transfer* of that job downstream — fire at rate 1e7 (effectively
1752% instant) and clear the blocked marker in the successor. The complementary become-blocked
1753% transition (b:0->1 when the destination is full) is added by the CTMC generator / SSA
1754% engine, since only they can see the destination's occupancy. Blocked marker = last state
1755% column when this station declares it (nvars col 2*R+1 == 1).
1756% The blocked marker shares nvars col 2*R+1 with the polling controller, so gate on
1757% the dedicated sn.isbasblocking field (set for the blocking station under BOTH the
1758% upstream and destination declaration forms) rather than the station's own drop
1759% rule, which fails for a destination-declared BAS. See BUG-83.
1760if event == EventType.DEP && ~isempty(sn.isbasblocking) && numel(sn.isbasblocking) >= ind ...
1761 && sn.isbasblocking(ind) == 1 ...
1762 && ~isempty(inspace) && inspace(1,end) == 1 && ~isempty(outspace)
1763 outspace(:,end) = 0;
1764 outrate(:) = 1e7;
1765end
1766
1767% Degraded service while the server is down: the scheduling handlers computed the
1768% completion rate from the up-server service process, so rescale it to the
1769% configured down-server rate. downRateScale is 1 whenever the server is up or no
1770% degraded rate was configured, so this is a no-op in every other model.
1771if downRateScale ~= 1 && ~isempty(outrate)
1772 outrate = outrate * downRateScale;
1773end
1774
1775end
Definition fjtag.m:161