LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ssa.m
1function [pi,SSq,arvRates,depRates,tranSysState,tranSync,sn]=solver_ssa(sn, init_state, options, eventCache)
2% [PI,SSQ,ARVRATES,DEPRATES,TRANSYSSTATE,QN]=SOLVER_SSA(QN,OPTIONS)
3
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7% by default the jobs are all initialized in the first valid state
8
9% Impatience support checks (mirror SolverCTMC): reneging supports only
10% exponential (memoryless) patience; balking supports only QUEUE_LENGTH.
11if isfield(sn,'impatienceClass') && ~isempty(sn.impatienceClass)
12 badRenege = (sn.impatienceClass==ImpatienceType.RENEGING) & (sn.impatienceType~=ProcessType.EXP);
13 if any(badRenege(:))
14 line_error(mfilename,'SolverSSA supports only exponential (memoryless) patience for reneging. Use SolverLDES or SolverJMT for phase-type patience.');
15 end
16end
17if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy)
18 badBalk = (sn.balkingStrategy~=0) & (sn.balkingStrategy~=BalkingStrategy.QUEUE_LENGTH);
19 if any(badBalk(:))
20 line_error(mfilename,'SolverSSA supports only QUEUE_LENGTH balking. Use SolverLDES or SolverJMT for wait-time-based balking.');
21 end
22end
23if isfield(sn,'retrialProc') && ~isempty(sn.retrialProc)
24 hasRetrial = ~cellfun(@isempty, sn.retrialProc);
25 if any(hasRetrial(:))
26 if any(hasRetrial(:) & (sn.retrialType(:)~=ProcessType.EXP))
27 line_error(mfilename,'SolverSSA supports only exponential (memoryless) retrial delay. Use SolverLDES or SolverMAM for phase-type retrials.');
28 end
29 if any(hasRetrial(:) & (sn.retrialMaxAttempts(:)>=0))
30 line_error(mfilename,'SolverSSA supports only unlimited retrials (maxAttempts=-1). Use SolverLDES for finite max-attempts.');
31 end
32 for ii = find(any(hasRetrial,2))'
33 served = 0;
34 for rr = 1:sn.nclasses
35 if ~isempty(sn.proc{ii}{rr}) && ~any(any(isnan(sn.proc{ii}{rr}{1})))
36 served = served + 1;
37 end
38 end
39 if served > 1
40 line_error(mfilename,'SolverSSA supports retrial only for single-class stations. Use SolverLDES for multi-class retrial.');
41 end
42 end
43 end
44end
45
46if ~isfield(options,'seed')
47 options.seed = 23000;
48end
49% Handle parallel computing toolbox gracefully - get worker index
50if isMATLABReleaseOlderThan("R2022b")
51 % Use labindex for older MATLAB versions
52 try
53 if ~isempty(getCurrentTask())
54 lab_idx = labindex(); %#ok<DLABINDEX>
55 else
56 lab_idx = 1;
57 end
58 catch
59 line_warning(mfilename,'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
60 lab_idx = 1;
61 end
62else
63 % Use spmdIndex for R2022b and newer (labindex is deprecated)
64 try
65 lab_idx = spmdIndex;
66 if isempty(lab_idx) || lab_idx == 0
67 lab_idx = 1;
68 end
69 catch
70 line_warning(mfilename,'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
71 lab_idx = 1;
72 end
73end
74Solver.resetRandomGeneratorSeed(options.seed + lab_idx - 1);
75
76%% generate local state spaces
77%nstations = sn.nstations;
78nstateful = sn.nstateful;
79%init_nserver = sn.nservers; % restore Inf at delay nodes
80R = sn.nclasses;
81N = sn.njobs';
82nnodes = sn.nnodes;
83sync = sn.sync;
84gsync = sn.gsync;
85
86line_debug('SSA solver starting: nstateful=%d, nclasses=%d, njobs=%s, samples=%d', nstateful, R, mat2str(N), options.samples);
87csmask = sn.csmask;
88
89cutoff = options.cutoff;
90if isscalar(cutoff)
91 cutoff = cutoff * ones(sn.nstations, sn.nclasses);
92end
93
94%%
95Np = N';
96capacityc = zeros(sn.nnodes, sn.nclasses);
97original_classcap = sn.classcap; % preserve original classcap for class switching scenarios
98for ind=1:sn.nnodes
99 if sn.isstation(ind) % place jobs across stations
100 ist = sn.nodeToStation(ind);
101 %isf = sn.nodeToStateful(ind);
102 for r=1:sn.nclasses %cut-off open classes to finite capacity
103 c = find(sn.chains(:,r));
104 % Check if visits is 0, but also preserve capacity for classes that can
105 % receive jobs via class switching (indicated by non-zero original classcap)
106 if isfield(sn,'fjclassmap') && ~isempty(sn.fjclassmap) && length(sn.fjclassmap) >= r && sn.fjclassmap(r) > 0
107 % FJ auxiliary sibling class: the per-tag multiplicity
108 % (tasksPerLink) can exceed the chain job population, so the
109 % adapter-set classcap is the correct capacity bound
110 capacityc(ind,r) = original_classcap(ist,r);
111 elseif ~isempty(sn.visits{c}) && sn.visits{c}(ist,r) == 0 && original_classcap(ist,r) == 0
112 capacityc(ind,r) = 0;
113 elseif ~isempty(sn.proc) && ~isempty(sn.proc{ist}{r}) && any(any(isnan(sn.proc{ist}{r}{1}))) && sn.nodetype(ind) ~= NodeType.Place % disabled (but not Place nodes)
114 capacityc(ind,r) = 0;
115 else
116 if isinf(N(r))
117 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
118 else
119 % closed classes: enumerate up to the chain population, but never
120 % beyond the class capacity at this station (finite-buffer stations)
121 capacityc(ind,r) = min(sum(sn.njobs(sn.chains(c,:))), sn.classcap(ist,r));
122 end
123 end
124 end
125 % never raise the station capacity above its configured total capacity
126 capacity_sum = min(sum(capacityc(ind,:)), sn.cap(ist));
127 if sn.sched(ist) == SchedStrategy.PAS
128 % Pass-and-swap stations are a single order-independent buffer of
129 % total size cap, not a sum of per-class buffers. Preserve the
130 % original total capacity, which is also the physical width used to
131 % size the ordered-list state (State.fromMarginal Wpas = cap).
132 capacity_sum = sn.cap(ist);
133 end
134 if isinf(sn.nservers(ist))
135 sn.nservers(ist) = capacity_sum;
136 end
137 sn.cap(ist,:) = capacity_sum;
138 sn.classcap(ist,:) = capacityc(ind,:);
139 end
140end
141% Signal (G-network negative-customer) classes never occupy a station; cap
142% their per-station capacity at 0 (except at the Source/EXT node) so the state
143% space does not enumerate unreachable states holding signal jobs.
144if isfield(sn,'issignal') && ~isempty(sn.issignal) && any(sn.issignal)
145 for ii = 1:sn.nstations
146 if sn.sched(ii) ~= SchedStrategy.EXT
147 sn.classcap(ii, sn.issignal(:)') = 0;
148 end
149 end
150end
151
152% Heterogeneous servers (single-class, ORDER policy) -> load-dependent rate
153% mu(n) = sum of the first min(n,c) server rates (mirrors SolverCTMC). Multi-
154% class heterogeneous servers are rejected (need per-server-type state).
155for ind = 1:sn.nnodes
156 if sn.isstation(ind) && isfield(sn,'nodeparam') && numel(sn.nodeparam) >= ind ...
157 && ~isempty(sn.nodeparam{ind}) && isstruct(sn.nodeparam{ind}) ...
158 && isfield(sn.nodeparam{ind},'nservertypes') && sn.nodeparam{ind}.nservertypes > 0
159 ist = sn.nodeToStation(ind);
160 np = sn.nodeparam{ind};
161 served = [];
162 for r = 1:sn.nclasses
163 if ~isempty(sn.proc{ist}{r}) && ~any(any(isnan(sn.proc{ist}{r}{1}))) && sn.rates(ist,r) > 0
164 served(end+1) = r; %#ok<AGROW>
165 end
166 end
167 if numel(served) > 1
168 line_error(mfilename,'SolverSSA supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
169 end
170 if numel(served) == 1
171 r = served;
172 srvrates = [];
173 for t = 1:np.nservertypes
174 if np.servercompat(t,r) && np.heterorates(t,r) > 0
175 srvrates = [srvrates, repmat(np.heterorates(t,r), 1, np.serverspertype(t))]; %#ok<AGROW>
176 end
177 end
178 c = numel(srvrates);
179 mu_base = sn.rates(ist,r);
180 if c > 0 && mu_base > 0
181 if isempty(sn.lldscaling)
182 sn.lldscaling = ones(sn.nstations, max([c, sum(sn.njobs(isfinite(sn.njobs))), 1]));
183 elseif size(sn.lldscaling,2) < c
184 sn.lldscaling(:, (size(sn.lldscaling,2)+1):c) = repmat(sn.lldscaling(:,end), 1, c-size(sn.lldscaling,2));
185 end
186 for n = 1:size(sn.lldscaling,2)
187 mun = sum(srvrates(1:min(n,c)));
188 sn.lldscaling(ist,n) = mun / (mu_base * min(n,c));
189 end
190 end
191 end
192 end
193end
194
195% Finite Capacity Region: precompute per-region member stations and caps. The
196% simulation blocks (holds upstream) any arrival that would push the aggregate
197% region population past a cap; see the FCR gate in the event loop below. This
198% is the SSA counterpart of the state-space filter in solver_ctmc.m.
199fcrOn = isfield(sn,'nregions') && sn.nregions > 0;
200if fcrOn
201 Kfcr = sn.nclasses;
202 fcrMembers = cell(sn.nregions,1);
203 fcrMemberMask= cell(sn.nregions,1);
204 fcrClassCap = cell(sn.nregions,1);
205 fcrGlobalCap = inf(sn.nregions,1);
206 fcrMemCap = inf(sn.nregions,1);
207 fcrSz = cell(sn.nregions,1);
208 fcrA = cell(sn.nregions,1);
209 fcrb = cell(sn.nregions,1);
210 for f = 1:sn.nregions
211 Rmat = sn.region{f}; % M x (K+1)
212 % membership: any job-count cap OR the region memory budget set on the
213 % station row (a memory-only region has all job-count entries at -1)
214 memvecFCR = -ones(sn.nstations,1);
215 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
216 memvecFCR = sn.regionmaxmem{f}(:);
217 end
218 mask = (any(Rmat ~= -1, 2) | memvecFCR ~= -1)';
219 fcrMemberMask{f} = mask;
220 fcrMembers{f} = find(mask);
221 ccap = inf(1,Kfcr);
222 for r = 1:Kfcr
223 cv = Rmat(fcrMembers{f}, r); cv = cv(cv ~= -1);
224 if ~isempty(cv); ccap(r) = min(cv); end
225 end
226 fcrClassCap{f} = ccap;
227 gv = Rmat(fcrMembers{f}, Kfcr+1); gv = gv(gv ~= -1);
228 if ~isempty(gv); fcrGlobalCap(f) = min(gv); end
229 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
230 mv = sn.regionmaxmem{f}(fcrMembers{f}); mv = mv(mv ~= -1);
231 if ~isempty(mv); fcrMemCap(f) = min(mv); end
232 end
233 fcrSz{f} = sn.regionsz(f,:);
234 if isfield(sn,'regionlincon') && size(sn.regionlincon,1) >= f && ~isempty(sn.regionlincon{f,1})
235 fcrA{f} = sn.regionlincon{f,1};
236 fcrb{f} = sn.regionlincon{f,2};
237 end
238 end
239 % WAITQ rule per (region, class): a refused entry parks in a per-region
240 % FIFO of (class, destination) tokens (JMT waiting-queue semantics) and
241 % re-enters head-of-line as capacity frees; DROP keeps censoring.
242 fcrRule = false(sn.nregions, Kfcr);
243 if isfield(sn,'regionrule') && ~isempty(sn.regionrule)
244 for f = 1:sn.nregions
245 for r = 1:Kfcr
246 fcrRule(f,r) = sn.regionrule(f,r) ~= DropStrategy.DROP;
247 end
248 end
249 end
250 fcrBuf = cell(sn.nregions,1);
251 for f = 1:sn.nregions
252 fcrBuf{f} = zeros(1,0);
253 end
254end
255
256%%
257if any(isinf(Np))
258 Np(isinf(Np)) = 0;
259end
260
261init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
262
263%%
264arvRatesSamples = zeros(options.samples,nstateful,R);
265depRatesSamples = zeros(options.samples,nstateful,R);
266A = length(sync);
267G = length(gsync);
268samples_collected = 1;
269nir = {};
270% fill stateCell with initial states
271cur_state = cell(nstateful,1); % cell array with current stateful node states
272for ind=1:sn.nnodes
273 if sn.isstateful(ind)
274 isf = sn.nodeToStateful(ind);
275 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
276 if sn.isstation(ind)
277 ist = sn.nodeToStation(ind);
278 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
279 nir{ist} = nir{ist}(:);
280 end
281 end
282end
283cur_state_1 = cur_state;
284% generate state vector
285state = cell2mat(cur_state');
286% create function to determine lengths of stateful node states
287statelen = cellfun(@length, cur_state);
288% data structures to save transient information - pre-allocate for all samples
289nSamples = options.samples;
290tranSync = zeros(nSamples,1);
291tranState = zeros(1+length(state), nSamples);
292tranState(1:(1+length(state)),1) = [0, state]';
293SSq = zeros(length(cell2mat(nir')), nSamples);
294SSq(:,1) = cell2mat(nir');
295local = sn.nnodes+1;
296last_node_a = 0; % active in the last occurred synchronization
297last_node_p = 0; % passive in the last occurred synchronization
298for act=1:A
299 node_a{act} = sync{act}.active{1}.node;
300 node_p{act} = sync{act}.passive{1}.node;
301 class_a{act} = sync{act}.active{1}.class;
302 class_p{act} = sync{act}.passive{1}.class;
303 event_a{act} = sync{act}.active{1}.event;
304 event_p{act} = sync{act}.passive{1}.event;
305 outprob_a{act} = [];
306 outprob_p{act} = [];
307 % Immediate-feedback self-loop detection (loop-invariant): a departure
308 % that routes back to the same FCFS-family station and arrives as a class
309 % flagged in sn.immfeed must hold the server instead of re-queueing behind
310 % the waiting jobs. The departure half is then computed with noPromote so
311 % the vacated server is re-entered by the fed-back job (see
312 % State.afterEventStation).
313 immfeed_selfloop{act} = false;
314 if event_a{act}==EventType.DEP && node_p{act}==node_a{act} ...
315 && node_a{act}>=1 && node_a{act}<=sn.nnodes && sn.isstation(node_a{act}) ...
316 && isfield(sn,'immfeed') && ~isempty(sn.immfeed)
317 istA_if = sn.nodeToStation(node_a{act});
318 if istA_if>=1 && istA_if<=size(sn.immfeed,1) ...
319 && class_p{act}>=1 && class_p{act}<=size(sn.immfeed,2) ...
320 && sn.immfeed(istA_if, class_p{act})
321 immfeed_selfloop{act} = true;
322 end
323 end
324end
325enabled_next_states = cell(1,A);
326
327%% Start main simulation loop
328isSimulation = true; % allow state vector to grow, e.g. for FCFS buffers
329% Precompute the loop-invariant afterEvent context. This MUST follow the
330% preamble above that rewrites sn.nservers/cap/classcap (Inf servers and
331% open-class cutoffs), so that blocking and rate computations match the
332% per-call derivation it replaces.
333aectx = State.afterEventInit(sn);
334samples_collected = 1;
335cur_time = 0;
336use_inline = true; % true = stable version, false = dev version
337
338try
339 while samples_collected < options.samples && cur_time <= options.timespan(2) && ~lineTimeoutExceeded(options)
340 %% This section corresponds to solver_ssa_findenabled in Java
341 %% Inlined for performance reasons
342 if use_inline
343 enabled_sync = []; % row is action label, col1=rate, col2=new state
344 enabled_rates = [];
345 enabled_fcr = zeros(0,4); % [region class dest isSwitch] FCR marker per transition
346 ctr = 1;
347 A = length(sync);
348 G = length(gsync);
349 % FCR: current aggregate per-class population of each region, used by
350 % the arrival gate below to block entries that would exceed a cap.
351 if fcrOn
352 xcurFCR = cell(sn.nregions,1);
353 for f = 1:sn.nregions
354 xf = zeros(1,sn.nclasses);
355 for i = fcrMembers{f}
356 ind_i = sn.stationToNode(i);
357 isf_i = sn.stationToStateful(i);
358 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
359 xf = xf + nir_i(:)';
360 end
361 xcurFCR{f} = xf;
362 end
363 end
364 for act=1:A
365 isf_a = sn.nodeToStateful(node_a{act});
366
367 enabled_next_states{act} = cur_state;
368 update_cond_a = true;
369 if update_cond_a
370 [enabled_next_states{act}{isf_a}, rate_a{act}, outprob_a{act}, eventCache] = State.afterEvent(sn, node_a{act}, cur_state{isf_a}, event_a{act}, class_a{act}, isSimulation, eventCache, aectx, immfeed_selfloop{act});
371 end
372
373 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
374 continue
375 end
376
377 for ia=1:size(enabled_next_states{act}{isf_a},1) % for all possible new states, check if they are enabled
378 % if the transition cannot occur
379 if isnan(rate_a{act}(ia)) || rate_a{act}(ia) == 0 % handles degenerate rate values
380 % set the transition with a zero rate so that it is
381 % never selected
382 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
383 end
384
385 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
386 continue
387 end
388 update_cond_p = true; %samples_collected == 1 || ((node_p{act} == last_node_a || node_p{act} == last_node_p)) || isempty(outprob_a{act}) || isempty(outprob_p{act});
389
390 if rate_a{act}(ia)>0
391 if node_p{act} ~= local
392 if node_p{act} == node_a{act} %self-loop, active and passive are the same
393 isf_p = isf_a;
394 if update_cond_p
395 [enabled_next_states{act}{isf_p}, ~, outprob_p{act}, eventCache] = State.afterEvent(sn, node_p{act}, enabled_next_states{act}{isf_p}, event_p{act}, class_p{act}, isSimulation, eventCache, aectx);
396 end
397 else % departure
398 isf_p = sn.nodeToStateful(node_p{act});
399 if update_cond_p
400 [enabled_next_states{act}{isf_p}, ~, outprob_p{act}, eventCache] = State.afterEvent(sn, node_p{act}, enabled_next_states{act}{isf_p}, event_p{act}, class_p{act}, isSimulation, eventCache, aectx);
401 end
402 end
403 if ~isempty(enabled_next_states{act}{isf_p})
404 if sn.isstatedep(node_a{act},3)
405 prob_sync_p{act} = sync{act}.passive{1}.prob(cur_state, enabled_next_states{act}); %state-dependent
406 else
407 prob_sync_p{act} = sync{act}.passive{1}.prob;
408 end
409 else
410 prob_sync_p{act} = 0;
411 end
412 end
413 if ~isempty(enabled_next_states{act}{isf_a})
414 if node_p{act} == local
415 prob_sync_p{act} = 1;
416 end
417 if ~isnan(rate_a{act})
418 if all(~cellfun(@isempty,enabled_next_states{act}))
419 % FCR gate: an arrival that would push a region's
420 % aggregate population past a cap is censored for
421 % DROP classes and parked in the region FIFO for
422 % WAITQ classes (active part applied only). A
423 % class-switching hop between two members of the
424 % same region is an exit followed by a gated
425 % re-entry (JMT ClassSwitch-outside-region parity).
426 blockFCR = false;
427 fcrMark = [0 0 0 0]; % [region class dest isSwitch]
428 if fcrOn && node_p{act} ~= local && node_p{act} <= sn.nnodes
429 jp = sn.nodeToStation(node_p{act});
430 if jp > 0
431 ja = sn.nodeToStation(node_a{act});
432 cc = class_p{act};
433 for f = 1:sn.nregions
434 mmask = fcrMemberMask{f};
435 if mmask(jp) && (ja <= 0 || ja > numel(mmask) || ~mmask(ja))
436 xn = xcurFCR{f}; xn(cc) = xn(cc) + 1;
437 if xn(cc) > fcrClassCap{f}(cc) || sum(xn) > fcrGlobalCap(f) ...
438 || (xn * fcrSz{f}(:) > fcrMemCap(f)) ...
439 || (~isempty(fcrA{f}) && any(fcrA{f} * xn(:) > fcrb{f}(:)))
440 if fcrRule(f,cc)
441 fcrMark = [f cc node_p{act} 0]; % park in FIFO
442 else
443 fcrMark = [f cc node_p{act} 2]; % DROP: destroyed
444 end
445 break;
446 end
447 elseif mmask(jp) && ja > 0 && ja <= numel(mmask) && mmask(ja) ...
448 && cc ~= class_a{act}
449 if fcrRule(f,cc)
450 fcrMark = [f cc node_p{act} 1]; % exit + gated re-entry
451 else
452 fcrMark = [f cc node_p{act} 3]; % exit + gated re-entry, DROP on refusal
453 end
454 break;
455 end
456 end
457 end
458 end
459 if fcrMark(1) > 0
460 % suppress the passive application: the job
461 % leaves the upstream node and its entry is
462 % resolved at application time
463 enabled_next_states{act}{isf_p} = cur_state{isf_p};
464 end
465 if event_a{act} == EventType.DEP && ~blockFCR
466 node_a_sf{act} = isf_a;
467 node_p_sf{act} = isf_p;
468 depRatesSamples(samples_collected,node_a_sf{act},class_a{act}) = depRatesSamples(samples_collected,node_a_sf{act},class_a{act}) + outprob_a{act} * outprob_p{act} * rate_a{act}(ia) * prob_sync_p{act};
469 arvRatesSamples(samples_collected,node_p_sf{act},class_p{act}) = arvRatesSamples(samples_collected,node_p_sf{act},class_p{act}) + outprob_a{act} * outprob_p{act} * rate_a{act}(ia) * prob_sync_p{act};
470 end
471 % simulate also self-loops as we need to log them
472 %if any(~cellfun(@isequal,new_state{act},cur_state))
473 if node_p{act} < local && ~sn.csmask(class_a{act}, class_p{act}) && sn.nodetype(node_p{act})~=NodeType.Source && (rate_a{act}(ia) * prob_sync_p{act} >0)
474 line_error(mfilename,sprintf('Error: state-dependent routing at node %d (%s) violates the class switching mask (node %d -> node %d, class %d -> class %d).', node_a{act}, sn.nodenames{node_a{act}}, node_a{act}, node_p{act}, class_a{act}, class_p{act}));
475 end
476 if ~blockFCR
477 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
478 enabled_sync(ctr) = act;
479 enabled_fcr(ctr,:) = fcrMark;
480 ctr = ctr + 1;
481 end
482 end
483 end
484 end
485 end
486 end
487 end
488 gctr_start = ctr;
489
490 for gact=1:G % event at node ind with global side-effects
491 gind = gsync{gact}.active{1}.node; % node index for global event
492 [enabled_next_states{A+gact}, outrate, outprob] = State.afterGlobalEvent(sn, gind, cur_state, gsync{gact}, isSimulation);
493 for ia=find(outrate .* outprob)
494 enabled_rates(ctr) = outrate(ia) * outprob(ia);
495 enabled_sync(ctr) = A+gact;
496 ctr = ctr + 1;
497
498 % Record departure/arrival rates for FIRE events at Places
499 if gsync{gact}.active{1}.event == EventType.FIRE
500 mode = gsync{gact}.active{1}.mode;
501 % Get enabling/firing conditions to determine affected classes
502 enabling_m = sn.nodeparam{gind}.enabling{mode};
503 firing_m = sn.nodeparam{gind}.firing{mode};
504
505 for j=1:length(gsync{gact}.passive)
506 pev = gsync{gact}.passive{j};
507 % Decode linear index to (node, class) - pev.node is a linear index from find() on enabling/firing matrix
508 [pev_node, pev_class] = ind2sub([sn.nnodes, R], pev.node);
509 if pev.event == EventType.PRE
510 % Departure from input Place (consuming tokens)
511 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
512 ep_isf = sn.nodeToStateful(pev_node);
513 % Record departures for the specific class from this PRE event
514 depRatesSamples(samples_collected, ep_isf, pev_class) = ...
515 depRatesSamples(samples_collected, ep_isf, pev_class) + outrate(ia) * outprob(ia);
516 end
517 elseif pev.event == EventType.POST
518 % Arrival at output Place (producing tokens)
519 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
520 fp_isf = sn.nodeToStateful(pev_node);
521 % Record arrivals for the specific class from this POST event
522 arvRatesSamples(samples_collected, fp_isf, pev_class) = ...
523 arvRatesSamples(samples_collected, fp_isf, pev_class) + outrate(ia) * outprob(ia);
524 end
525 end
526 end
527 end
528 end
529 end
530
531 % fork firing synchronizations (native fork-join support): the
532 % firing consumes the parent held at the Fork and emits one
533 % sibling per branch atomically; indices beyond gctr_start take
534 % the global-event path in the bookkeeping below
535 FJ = 0;
536 if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
537 FJ = length(sn.fjsync);
538 end
539 for fjact=1:FJ
540 [fjStates, fjrate, fjprob] = State.afterFJEvent(sn, sn.fjsync{fjact}, cur_state, isSimulation);
541 if ~isempty(fjStates)
542 enabled_next_states{A+G+fjact} = fjStates{1};
543 enabled_rates(ctr) = fjrate(1) * fjprob(1);
544 enabled_sync(ctr) = A+G+fjact;
545 ctr = ctr + 1;
546 fjentry = sn.fjsync{fjact};
547 isf_fork = sn.nodeToStateful(fjentry.fork);
548 depRatesSamples(samples_collected, isf_fork, fjentry.class) = ...
549 depRatesSamples(samples_collected, isf_fork, fjentry.class) + fjrate(1) * fjprob(1);
550 for b=1:length(fjentry.branchheads)
551 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
552 arvRatesSamples(samples_collected, isf_bh, fjentry.auxclasses(b)) = ...
553 arvRatesSamples(samples_collected, isf_bh, fjentry.auxclasses(b)) + fjrate(1) * fjprob(1);
554 end
555 end
556 end
557 else
558 [enabled_next_states,enabled_rates,enabled_sync,gctr_start,depRatesSamples,arvRatesSamples,outprob_a,outprob_p,rate_a, eventCache] = solver_ssa_findenabled(sn,node_a,enabled_next_states,cur_state,outprob_a,event_a,class_a,isSimulation,node_p,local,outprob_p,event_p,class_p,sync,gsync,depRatesSamples,samples_collected,arvRatesSamples,last_node_a,last_node_p,eventCache);
559 end
560 %% Gillespie direct method
561 tot_rate = sum(enabled_rates);
562 cum_rate = cumsum(enabled_rates) / tot_rate;
563 selected_transition = 1 + max([0,find( rand > cum_rate )]); % select action
564
565 % Update record of last active/passive pair
566 if isempty(enabled_sync)
567 line_error(mfilename,'SSA simulation entered a deadlock before collecting all samples, no synchronization is enabled.');
568 end
569 if selected_transition < gctr_start
570 % regular event pair
571 last_node_a = node_a{enabled_sync(selected_transition)};
572 last_node_p = node_p{enabled_sync(selected_transition)};
573 else % global event
574 last_node_a = NaN;
575 last_node_p = NaN;
576 end
577
578 %% Update paddings
579 % This part is needed to ensure that when the state vector grows the
580 % padding of zero is done on the left (e.g., for FCFS buffers)
581 for ind=1:sn.nnodes
582 if sn.isstation(ind)
583 isf = sn.nodeToStateful(ind);
584 deltalen = length(cur_state{isf}) - statelen(isf);
585 if deltalen>0
586 statelen(isf) = length(cur_state{isf});
587 % here do padding
588 if ind==1
589 shift = 0;
590 else
591 shift = sum(statelen(1:isf-1));
592 end
593 pad = zeros(deltalen, size(tranState,2));
594 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
595 end
596 end
597 end
598
599 %% Simulate the time increment
600 state = cell2mat(cur_state');
601 dt = -(log(rand)/tot_rate);
602 cur_time = cur_time + dt;
603
604 %% Save simulation output data
605 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
606 tranSync(samples_collected,1) = enabled_sync(selected_transition);
607 for ind=1:sn.nnodes
608 if sn.isstation(ind)
609 isf = sn.nodeToStateful(ind);
610 ist = sn.nodeToStation(ind);
611 [~,nir{ist}] = State.toMarginal(sn, ind, cur_state{isf});
612 nir{ist}=nir{ist}(:);
613 end
614 end
615 SSq(:,samples_collected) = cell2mat(nir');
616
617 %% Update current state and sample counter
618 cur_state_1 = cur_state;
619 cur_state = enabled_next_states{enabled_sync(selected_transition)};
620
621 % KCHOICES with memory: record the destination just chosen so the
622 % next dispatch consults it. Only applies to Router-typed active
623 % nodes that have KCHOICES routing with withMemory=true. This is the
624 % MATLAB-side mirror of LDES's kchoicesLastSelected tracking.
625 if selected_transition < gctr_start
626 actSync = enabled_sync(selected_transition);
627 if ~isempty(node_a{actSync}) && ~isempty(node_p{actSync}) ...
628 && node_a{actSync} >= 1 && node_a{actSync} <= sn.nnodes
629 aNode = node_a{actSync};
630 aClass = class_a{actSync};
631 if sn.nodetype(aNode) == NodeType.Router && aClass >= 1 ...
632 && sn.routing(aNode, aClass) == RoutingStrategy.KCHOICES
633 np_a = [];
634 if iscell(sn.nodeparam) && aNode <= numel(sn.nodeparam) ...
635 && iscell(sn.nodeparam{aNode}) ...
636 && aClass <= numel(sn.nodeparam{aNode})
637 np_a = sn.nodeparam{aNode}{aClass};
638 end
639 if ~isempty(np_a) && isfield(np_a, 'withMemory') && np_a.withMemory
640 isf_a = sn.nodeToStateful(aNode);
641 Rcl = sn.nclasses;
642 if ~isempty(cur_state{isf_a}) && length(cur_state{isf_a}) >= Rcl
643 cur_state{isf_a}(end - Rcl + aClass) = node_p{actSync};
644 end
645 end
646 end
647 end
648 end
649
650 %% FCR WAITQ bookkeeping: park blocked entries in the region FIFO,
651 % release FIFO heads freed by the applied transition (strict FIFO,
652 % head-of-line), and resolve pending class-switch re-entries
653 if fcrOn && use_inline
654 pk = [0 0 0 0];
655 if selected_transition <= size(enabled_fcr,1)
656 pk = enabled_fcr(selected_transition,:);
657 end
658 if pk(1) > 0 && pk(4) == 0
659 % blocked entry: park the (class, destination) token
660 fcrBuf{pk(1)}(end+1) = (pk(3)-1)*R + pk(2);
661 end
662 % pk(4)==2: DROP, the refused job was destroyed (active part only)
663 % release cascade over all regions
664 [cur_state, fcrBuf, eventCache] = fcr_release(sn, cur_state, fcrBuf, ...
665 fcrMembers, fcrClassCap, fcrGlobalCap, fcrMemCap, fcrSz, fcrA, fcrb, ...
666 isSimulation, eventCache, aectx);
667 if pk(1) > 0 && (pk(4) == 1 || pk(4) == 3)
668 % class-switching hop: gate the re-entry after the cascade
669 f_ = pk(1); cls_ = pk(2); dest_ = pk(3);
670 xf_ = fcr_regionpop(sn, cur_state, fcrMembers{f_});
671 xn_ = xf_; xn_(cls_) = xn_(cls_) + 1;
672 admitted = false;
673 if ~fcr_violates(xn_, fcrClassCap{f_}, fcrGlobalCap(f_), fcrMemCap(f_), fcrSz{f_}, fcrA{f_}, fcrb{f_})
674 isf_d = sn.nodeToStateful(dest_);
675 [ns_, ~, ~, eventCache] = State.afterEvent(sn, dest_, cur_state{isf_d}, EventType.ARV, cls_, isSimulation, eventCache, aectx);
676 if ~isempty(ns_)
677 cur_state{isf_d} = ns_(1,:);
678 admitted = true;
679 end
680 end
681 if ~admitted && pk(4) == 1
682 fcrBuf{f_}(end+1) = (dest_-1)*R + cls_;
683 end
684 % pk(4)==3 refused: DROP, the switching job is destroyed
685 end
686 end
687
688 samples_collected = samples_collected + 1;
689
690 %% Print progress
691 print_progress(options,samples_collected);
692 end
693 % Print newline after progress counter
694 if options.verbose
695 line_printf('\n');
696 end
697catch ME
698 getReport(ME)
699end
700
701% Trim pre-allocated arrays to actual number of samples collected
702samples_collected = samples_collected - 1; % Adjust for the increment at end of loop
703tranState = tranState(:, 1:samples_collected);
704tranSync = tranSync(1:samples_collected, :);
705SSq = SSq(:, 1:samples_collected);
706
707% Warmup discard: drop the first floor(warmupfrac * samples) samples so that
708% steady-state averages are not biased by transient behaviour. Controlled by
709% options.config.warmupfrac (default 0.0 = no discard).
710warmupfrac = 0.0;
711if isfield(options, 'config') && isfield(options.config, 'warmupfrac')
712 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
713end
714if warmupfrac > 0 && samples_collected > 1
715 nDrop = floor(warmupfrac * samples_collected);
716 if nDrop > 0 && nDrop < samples_collected
717 keep = (nDrop+1):samples_collected;
718 tranState = tranState(:, keep);
719 tranSync = tranSync(keep, :);
720 SSq = SSq(:, keep);
721 if exist('arvRatesSamples', 'var') && ~isempty(arvRatesSamples) ...
722 && size(arvRatesSamples, 1) >= samples_collected
723 arvRatesSamples = arvRatesSamples(keep, :, :);
724 end
725 if exist('depRatesSamples', 'var') && ~isempty(depRatesSamples) ...
726 && size(depRatesSamples, 1) >= samples_collected
727 depRatesSamples = depRatesSamples(keep, :, :);
728 end
729 samples_collected = numel(keep);
730 end
731end
732
733tranState = tranState';
734
735
736[u,ui,uj] = unique(tranState(:,2:end),'rows');
737statesz = cellfun(@length, cur_state_1)';
738tranSysState = cell(1,length(cur_state)+1);
739tranSysState{1} = cumsum(tranState(:,1));
740for j=1:length(statesz)
741 tranSysState{1+j} = tranState(:,1+(1+sum(statesz(1:(j-1)))):(1+sum(statesz(1:j))));
742end
743arvRates = zeros(size(u,1),sn.nstateful,R);
744depRates = zeros(size(u,1),sn.nstateful,R);
745
746pi = zeros(1,size(u,1));
747for s=1:size(u,1)
748 pi(s) = sum(tranState(uj==s,1));
749end
750SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
751
752for ind=1:sn.nnodes
753 if sn.isstateful(ind)
754 isf = sn.nodeToStateful(ind);
755 if sn.isstation(ind)
756 ist = sn.nodeToStation(ind);
757 %K = sn.phasessz(ist,:);
758 %Ks = sn.phaseshift(ist,:);
759 end
760 for s=1:size(u,1)
761 for r=1:R
762 arvRates(s,isf,r) = arvRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate is enough here
763 depRates(s,isf,r) = depRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate is enough here
764 end
765 end
766 end
767end
768pi = pi/sum(pi);
769%sn.nservers = init_nserver; % restore Inf at delay nodes
770end
771
772function print_progress(options,samples_collected)
773if options.verbose && ~batchStartupOptionUsed
774 if samples_collected == 1e2
775 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
776 elseif options.verbose == 2
777 if samples_collected == 0
778 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
779 else
780 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
781 end
782 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
783 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
784 end
785end
786end
787function x = fcr_regionpop(sn, cur_state, members)
788% X=FCR_REGIONPOP(SN,CUR_STATE,MEMBERS) per-class population of a finite
789% capacity region given the current state cells
790x = zeros(1, sn.nclasses);
791for i = members
792 ind_i = sn.stationToNode(i);
793 isf_i = sn.stationToStateful(i);
794 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
795 x = x + nir_i(:)';
796end
797end
798
799function tf = fcr_violates(xn, ccap, gcap, memcap, sz, A, b)
800% TF=FCR_VIOLATES(...) true if population vector xn breaks any admission
801% constraint of the region
802tf = any(xn > ccap) || sum(xn) > gcap || (xn * sz(:) > memcap);
803if ~tf && ~isempty(A)
804 tf = any(A * xn(:) > b(:));
805end
806end
807
808function [cur_state, fcrBuf, eventCache] = fcr_release(sn, cur_state, fcrBuf, ...
809 fcrMembers, fcrClassCap, fcrGlobalCap, fcrMemCap, fcrSz, fcrA, fcrb, ...
810 isSimulation, eventCache, aectx)
811% FCR_RELEASE strict-FIFO head-of-line release of parked region tokens:
812% admit heads while the admission constraints permit, applying the arrival
813% to the destination station state
814K = sn.nclasses;
815progress = true;
816while progress
817 progress = false;
818 for f = 1:length(fcrBuf)
819 if isempty(fcrBuf{f})
820 continue
821 end
822 x = fcr_regionpop(sn, cur_state, fcrMembers{f});
823 tok = fcrBuf{f}(1);
824 dest = floor((tok-1)/K) + 1;
825 r = mod(tok-1, K) + 1;
826 xn = x; xn(r) = xn(r) + 1;
827 if fcr_violates(xn, fcrClassCap{f}, fcrGlobalCap(f), fcrMemCap(f), fcrSz{f}, fcrA{f}, fcrb{f})
828 continue % head-of-line: this region's FIFO stays blocked
829 end
830 isf_d = sn.nodeToStateful(dest);
831 [ns, ~, ~, eventCache] = State.afterEvent(sn, dest, cur_state{isf_d}, EventType.ARV, r, isSimulation, eventCache, aectx);
832 if isempty(ns)
833 continue % destination cannot accept (e.g. station capacity)
834 end
835 cur_state{isf_d} = ns(1,:);
836 fcrBuf{f}(1) = [];
837 progress = true;
838 end
839end
840end
Definition fjtag.m:157
Definition Station.m:245