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 % see _kb/06-solver-catalog.md for rationale (native fork-join)
108 capacityc(ind,r) = original_classcap(ist,r);
109 elseif ~isempty(sn.visits{c}) && sn.visits{c}(ist,r) == 0 && original_classcap(ist,r) == 0
110 capacityc(ind,r) = 0;
111 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)
112 capacityc(ind,r) = 0;
113 else
114 if isinf(N(r))
115 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
116 else
117 % closed classes: enumerate up to the chain population, but never
118 % beyond the class capacity at this station (finite-buffer stations)
119 capacityc(ind,r) = min(sum(sn.njobs(sn.chains(c,:))), sn.classcap(ist,r));
120 end
121 end
122 end
123 % never raise the station capacity above its configured total capacity
124 capacity_sum = min(sum(capacityc(ind,:)), sn.cap(ist));
125 if sn.sched(ist) == SchedStrategy.PAS
126 % see _kb/06-solver-catalog.md for rationale (SSA PAS capacity)
127 capacity_sum = sn.cap(ist);
128 end
129 if isinf(sn.nservers(ist))
130 sn.nservers(ist) = capacity_sum;
131 end
132 sn.cap(ist,:) = capacity_sum;
133 sn.classcap(ist,:) = capacityc(ind,:);
134 end
135end
136% see _kb/06-solver-catalog.md for rationale (SSA G-network signal capacity)
137if isfield(sn,'issignal') && ~isempty(sn.issignal) && any(sn.issignal)
138 for ii = 1:sn.nstations
139 if sn.sched(ii) ~= SchedStrategy.EXT
140 sn.classcap(ii, sn.issignal(:)') = 0;
141 end
142 end
143end
144
145% see _kb/06-solver-catalog.md for rationale (SSA heterogeneous servers)
146for ind = 1:sn.nnodes
147 if sn.isstation(ind) && isfield(sn,'nodeparam') && numel(sn.nodeparam) >= ind ...
148 && ~isempty(sn.nodeparam{ind}) && isstruct(sn.nodeparam{ind}) ...
149 && isfield(sn.nodeparam{ind},'nservertypes') && sn.nodeparam{ind}.nservertypes > 0
150 ist = sn.nodeToStation(ind);
151 np = sn.nodeparam{ind};
152 served = [];
153 for r = 1:sn.nclasses
154 if ~isempty(sn.proc{ist}{r}) && ~any(any(isnan(sn.proc{ist}{r}{1}))) && sn.rates(ist,r) > 0
155 served(end+1) = r; %#ok<AGROW>
156 end
157 end
158 if numel(served) > 1
159 line_error(mfilename,'SolverSSA supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
160 end
161 if numel(served) == 1
162 r = served;
163 srvrates = [];
164 for t = 1:np.nservertypes
165 if np.servercompat(t,r) && np.heterorates(t,r) > 0
166 srvrates = [srvrates, repmat(np.heterorates(t,r), 1, np.serverspertype(t))]; %#ok<AGROW>
167 end
168 end
169 c = numel(srvrates);
170 mu_base = sn.rates(ist,r);
171 if c > 0 && mu_base > 0
172 if isempty(sn.lldscaling)
173 sn.lldscaling = ones(sn.nstations, max([c, sum(sn.njobs(isfinite(sn.njobs))), 1]));
174 elseif size(sn.lldscaling,2) < c
175 sn.lldscaling(:, (size(sn.lldscaling,2)+1):c) = repmat(sn.lldscaling(:,end), 1, c-size(sn.lldscaling,2));
176 end
177 for n = 1:size(sn.lldscaling,2)
178 mun = sum(srvrates(1:min(n,c)));
179 sn.lldscaling(ist,n) = mun / (mu_base * min(n,c));
180 end
181 end
182 end
183 end
184end
185
186% see _kb/06-solver-catalog.md for rationale (SSA FCR)
187fcrOn = isfield(sn,'nregions') && sn.nregions > 0;
188if fcrOn
189 Kfcr = sn.nclasses;
190 fcrMembers = cell(sn.nregions,1);
191 fcrMemberMask= cell(sn.nregions,1);
192 fcrClassCap = cell(sn.nregions,1);
193 fcrGlobalCap = inf(sn.nregions,1);
194 fcrMemCap = inf(sn.nregions,1);
195 fcrSz = cell(sn.nregions,1);
196 fcrA = cell(sn.nregions,1);
197 fcrb = cell(sn.nregions,1);
198 for f = 1:sn.nregions
199 Rmat = sn.region{f}; % M x (K+1)
200 % membership: any job-count cap OR the region memory budget set on the
201 % station row (a memory-only region has all job-count entries at -1)
202 memvecFCR = -ones(sn.nstations,1);
203 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
204 memvecFCR = sn.regionmaxmem{f}(:);
205 end
206 mask = (any(Rmat ~= -1, 2) | memvecFCR ~= -1)';
207 fcrMemberMask{f} = mask;
208 fcrMembers{f} = find(mask);
209 ccap = inf(1,Kfcr);
210 for r = 1:Kfcr
211 cv = Rmat(fcrMembers{f}, r); cv = cv(cv ~= -1);
212 if ~isempty(cv); ccap(r) = min(cv); end
213 end
214 fcrClassCap{f} = ccap;
215 gv = Rmat(fcrMembers{f}, Kfcr+1); gv = gv(gv ~= -1);
216 if ~isempty(gv); fcrGlobalCap(f) = min(gv); end
217 if isfield(sn,'regionmaxmem') && numel(sn.regionmaxmem) >= f && ~isempty(sn.regionmaxmem{f})
218 mv = sn.regionmaxmem{f}(fcrMembers{f}); mv = mv(mv ~= -1);
219 if ~isempty(mv); fcrMemCap(f) = min(mv); end
220 end
221 fcrSz{f} = sn.regionsz(f,:);
222 if isfield(sn,'regionlincon') && size(sn.regionlincon,1) >= f && ~isempty(sn.regionlincon{f,1})
223 fcrA{f} = sn.regionlincon{f,1};
224 fcrb{f} = sn.regionlincon{f,2};
225 end
226 end
227 % see _kb/06-solver-catalog.md for rationale (SSA FCR)
228 fcrRule = false(sn.nregions, Kfcr);
229 if isfield(sn,'regionrule') && ~isempty(sn.regionrule)
230 for f = 1:sn.nregions
231 for r = 1:Kfcr
232 fcrRule(f,r) = sn.regionrule(f,r) ~= DropStrategy.DROP;
233 end
234 end
235 end
236 fcrBuf = cell(sn.nregions,1);
237 for f = 1:sn.nregions
238 fcrBuf{f} = zeros(1,0);
239 end
240end
241
242%%
243if any(isinf(Np))
244 Np(isinf(Np)) = 0;
245end
246
247init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
248
249%%
250arvRatesSamples = zeros(options.samples,nstateful,R);
251depRatesSamples = zeros(options.samples,nstateful,R);
252A = length(sync);
253G = length(gsync);
254samples_collected = 1;
255nir = {};
256% fill stateCell with initial states
257cur_state = cell(nstateful,1); % cell array with current stateful node states
258for ind=1:sn.nnodes
259 if sn.isstateful(ind)
260 isf = sn.nodeToStateful(ind);
261 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
262 if sn.isstation(ind)
263 ist = sn.nodeToStation(ind);
264 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
265 nir{ist} = nir{ist}(:);
266 end
267 end
268end
269cur_state_1 = cur_state;
270% generate state vector
271state = cell2mat(cur_state');
272% create function to determine lengths of stateful node states
273statelen = cellfun(@length, cur_state);
274% data structures to save transient information - pre-allocate for all samples
275nSamples = options.samples;
276tranSync = zeros(nSamples,1);
277tranState = zeros(1+length(state), nSamples);
278tranState(1:(1+length(state)),1) = [0, state]';
279SSq = zeros(length(cell2mat(nir')), nSamples);
280SSq(:,1) = cell2mat(nir');
281local = sn.nnodes+1;
282last_node_a = 0; % active in the last occurred synchronization
283last_node_p = 0; % passive in the last occurred synchronization
284for act=1:A
285 node_a{act} = sync{act}.active{1}.node;
286 node_p{act} = sync{act}.passive{1}.node;
287 class_a{act} = sync{act}.active{1}.class;
288 class_p{act} = sync{act}.passive{1}.class;
289 event_a{act} = sync{act}.active{1}.event;
290 event_p{act} = sync{act}.passive{1}.event;
291 outprob_a{act} = [];
292 outprob_p{act} = [];
293 % see _kb/06-solver-catalog.md for rationale (SSA immfeed self-loop)
294 immfeed_selfloop{act} = false;
295 if event_a{act}==EventType.DEP && node_p{act}==node_a{act} ...
296 && node_a{act}>=1 && node_a{act}<=sn.nnodes && sn.isstation(node_a{act}) ...
297 && isfield(sn,'immfeed') && ~isempty(sn.immfeed)
298 istA_if = sn.nodeToStation(node_a{act});
299 if istA_if>=1 && istA_if<=size(sn.immfeed,1) ...
300 && class_p{act}>=1 && class_p{act}<=size(sn.immfeed,2) ...
301 && sn.immfeed(istA_if, class_p{act})
302 immfeed_selfloop{act} = true;
303 end
304 end
305end
306enabled_next_states = cell(1,A);
307
308%% Start main simulation loop
309isSimulation = true; % allow state vector to grow, e.g. for FCFS buffers
310% see _kb/06-solver-catalog.md for rationale (SSA preamble ordering)
311aectx = State.afterEventInit(sn);
312samples_collected = 1;
313cur_time = 0;
314use_inline = true; % true = stable version, false = dev version
315
316try
317 while samples_collected < options.samples && cur_time <= options.timespan(2) && ~lineTimeoutExceeded(options)
318 %% This section corresponds to solver_ssa_findenabled in Java
319 %% Inlined for performance reasons
320 if use_inline
321 enabled_sync = []; % row is action label, col1=rate, col2=new state
322 enabled_rates = [];
323 enabled_fcr = zeros(0,4); % [region class dest isSwitch] FCR marker per transition
324 ctr = 1;
325 A = length(sync);
326 G = length(gsync);
327 % FCR: current aggregate per-class population of each region, used by
328 % the arrival gate below to block entries that would exceed a cap.
329 if fcrOn
330 xcurFCR = cell(sn.nregions,1);
331 for f = 1:sn.nregions
332 xf = zeros(1,sn.nclasses);
333 for i = fcrMembers{f}
334 ind_i = sn.stationToNode(i);
335 isf_i = sn.stationToStateful(i);
336 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
337 xf = xf + nir_i(:)';
338 end
339 xcurFCR{f} = xf;
340 end
341 end
342 for act=1:A
343 isf_a = sn.nodeToStateful(node_a{act});
344
345 enabled_next_states{act} = cur_state;
346 update_cond_a = true;
347 if update_cond_a
348 [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});
349 end
350
351 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
352 continue
353 end
354
355 for ia=1:size(enabled_next_states{act}{isf_a},1) % for all possible new states, check if they are enabled
356 % if the transition cannot occur
357 if isnan(rate_a{act}(ia)) || rate_a{act}(ia) == 0 % handles degenerate rate values
358 % set the transition with a zero rate so that it is
359 % never selected
360 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
361 end
362
363 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
364 continue
365 end
366 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});
367
368 if rate_a{act}(ia)>0
369 if node_p{act} ~= local
370 if node_p{act} == node_a{act} %self-loop, active and passive are the same
371 isf_p = isf_a;
372 if update_cond_p
373 [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);
374 end
375 else % departure
376 isf_p = sn.nodeToStateful(node_p{act});
377 if update_cond_p
378 [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);
379 end
380 end
381 if ~isempty(enabled_next_states{act}{isf_p})
382 if sn.isstatedep(node_a{act},3)
383 prob_sync_p{act} = sync{act}.passive{1}.prob(cur_state, enabled_next_states{act}); %state-dependent
384 else
385 prob_sync_p{act} = sync{act}.passive{1}.prob;
386 end
387 else
388 prob_sync_p{act} = 0;
389 end
390 end
391 if ~isempty(enabled_next_states{act}{isf_a})
392 if node_p{act} == local
393 prob_sync_p{act} = 1;
394 end
395 if ~isnan(rate_a{act})
396 if all(~cellfun(@isempty,enabled_next_states{act}))
397 % see _kb/06-solver-catalog.md for rationale (SSA FCR)
398 blockFCR = false;
399 fcrMark = [0 0 0 0]; % [region class dest isSwitch]
400 if fcrOn && node_p{act} ~= local && node_p{act} <= sn.nnodes
401 jp = sn.nodeToStation(node_p{act});
402 if jp > 0
403 ja = sn.nodeToStation(node_a{act});
404 cc = class_p{act};
405 for f = 1:sn.nregions
406 mmask = fcrMemberMask{f};
407 if mmask(jp) && (ja <= 0 || ja > numel(mmask) || ~mmask(ja))
408 xn = xcurFCR{f}; xn(cc) = xn(cc) + 1;
409 if xn(cc) > fcrClassCap{f}(cc) || sum(xn) > fcrGlobalCap(f) ...
410 || (xn * fcrSz{f}(:) > fcrMemCap(f)) ...
411 || (~isempty(fcrA{f}) && any(fcrA{f} * xn(:) > fcrb{f}(:)))
412 if fcrRule(f,cc)
413 fcrMark = [f cc node_p{act} 0]; % park in FIFO
414 else
415 fcrMark = [f cc node_p{act} 2]; % DROP: destroyed
416 end
417 break;
418 end
419 elseif mmask(jp) && ja > 0 && ja <= numel(mmask) && mmask(ja) ...
420 && cc ~= class_a{act}
421 if fcrRule(f,cc)
422 fcrMark = [f cc node_p{act} 1]; % exit + gated re-entry
423 else
424 fcrMark = [f cc node_p{act} 3]; % exit + gated re-entry, DROP on refusal
425 end
426 break;
427 end
428 end
429 end
430 end
431 if fcrMark(1) > 0
432 % see _kb/06-solver-catalog.md for rationale (SSA FCR)
433 enabled_next_states{act}{isf_p} = cur_state{isf_p};
434 end
435 if event_a{act} == EventType.DEP && ~blockFCR
436 node_a_sf{act} = isf_a;
437 node_p_sf{act} = isf_p;
438 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};
439 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};
440 end
441 % simulate also self-loops as we need to log them
442 %if any(~cellfun(@isequal,new_state{act},cur_state))
443 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)
444 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}));
445 end
446 if ~blockFCR
447 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
448 enabled_sync(ctr) = act;
449 enabled_fcr(ctr,:) = fcrMark;
450 ctr = ctr + 1;
451 end
452 end
453 end
454 end
455 end
456 end
457 end
458 gctr_start = ctr;
459
460 for gact=1:G % event at node ind with global side-effects
461 gind = gsync{gact}.active{1}.node; % node index for global event
462 [enabled_next_states{A+gact}, outrate, outprob] = State.afterGlobalEvent(sn, gind, cur_state, gsync{gact}, isSimulation);
463 for ia=find(outrate .* outprob)
464 enabled_rates(ctr) = outrate(ia) * outprob(ia);
465 enabled_sync(ctr) = A+gact;
466 ctr = ctr + 1;
467
468 % Record departure/arrival rates for FIRE events at Places
469 if gsync{gact}.active{1}.event == EventType.FIRE
470 mode = gsync{gact}.active{1}.mode;
471 % Get enabling/firing conditions to determine affected classes
472 enabling_m = sn.nodeparam{gind}.enabling{mode};
473 firing_m = sn.nodeparam{gind}.firing{mode};
474
475 for j=1:length(gsync{gact}.passive)
476 pev = gsync{gact}.passive{j};
477 % Decode linear index to (node, class) - pev.node is a linear index from find() on enabling/firing matrix
478 [pev_node, pev_class] = ind2sub([sn.nnodes, R], pev.node);
479 if pev.event == EventType.PRE
480 % Departure from input Place (consuming tokens)
481 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
482 ep_isf = sn.nodeToStateful(pev_node);
483 % Record departures for the specific class from this PRE event
484 depRatesSamples(samples_collected, ep_isf, pev_class) = ...
485 depRatesSamples(samples_collected, ep_isf, pev_class) + outrate(ia) * outprob(ia);
486 end
487 elseif pev.event == EventType.POST
488 % Arrival at output Place (producing tokens)
489 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
490 fp_isf = sn.nodeToStateful(pev_node);
491 % Record arrivals for the specific class from this POST event
492 arvRatesSamples(samples_collected, fp_isf, pev_class) = ...
493 arvRatesSamples(samples_collected, fp_isf, pev_class) + outrate(ia) * outprob(ia);
494 end
495 end
496 end
497 end
498 end
499 end
500
501 % see _kb/06-solver-catalog.md for rationale (native fork-join)
502 FJ = 0;
503 if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
504 FJ = length(sn.fjsync);
505 end
506 for fjact=1:FJ
507 [fjStates, fjrate, fjprob] = State.afterFJEvent(sn, sn.fjsync{fjact}, cur_state, isSimulation);
508 if ~isempty(fjStates)
509 enabled_next_states{A+G+fjact} = fjStates{1};
510 enabled_rates(ctr) = fjrate(1) * fjprob(1);
511 enabled_sync(ctr) = A+G+fjact;
512 ctr = ctr + 1;
513 fjentry = sn.fjsync{fjact};
514 isf_fork = sn.nodeToStateful(fjentry.fork);
515 depRatesSamples(samples_collected, isf_fork, fjentry.class) = ...
516 depRatesSamples(samples_collected, isf_fork, fjentry.class) + fjrate(1) * fjprob(1);
517 for b=1:length(fjentry.branchheads)
518 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
519 arvRatesSamples(samples_collected, isf_bh, fjentry.auxclasses(b)) = ...
520 arvRatesSamples(samples_collected, isf_bh, fjentry.auxclasses(b)) + fjrate(1) * fjprob(1);
521 end
522 end
523 end
524 else
525 [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);
526 end
527 %% Gillespie direct method
528 tot_rate = sum(enabled_rates);
529 cum_rate = cumsum(enabled_rates) / tot_rate;
530 selected_transition = 1 + max([0,find( rand > cum_rate )]); % select action
531
532 % Update record of last active/passive pair
533 if isempty(enabled_sync)
534 line_error(mfilename,'SSA simulation entered a deadlock before collecting all samples, no synchronization is enabled.');
535 end
536 if selected_transition < gctr_start
537 % regular event pair
538 last_node_a = node_a{enabled_sync(selected_transition)};
539 last_node_p = node_p{enabled_sync(selected_transition)};
540 else % global event
541 last_node_a = NaN;
542 last_node_p = NaN;
543 end
544
545 %% Update paddings
546 % see _kb/06-solver-catalog.md for rationale (SSA state left-padding)
547 for ind=1:sn.nnodes
548 if sn.isstation(ind)
549 isf = sn.nodeToStateful(ind);
550 deltalen = length(cur_state{isf}) - statelen(isf);
551 if deltalen>0
552 statelen(isf) = length(cur_state{isf});
553 % here do padding
554 if ind==1
555 shift = 0;
556 else
557 shift = sum(statelen(1:isf-1));
558 end
559 pad = zeros(deltalen, size(tranState,2));
560 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
561 end
562 end
563 end
564
565 %% Simulate the time increment
566 state = cell2mat(cur_state');
567 dt = -(log(rand)/tot_rate);
568 cur_time = cur_time + dt;
569
570 %% Save simulation output data
571 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
572 tranSync(samples_collected,1) = enabled_sync(selected_transition);
573 for ind=1:sn.nnodes
574 if sn.isstation(ind)
575 isf = sn.nodeToStateful(ind);
576 ist = sn.nodeToStation(ind);
577 [~,nir{ist}] = State.toMarginal(sn, ind, cur_state{isf});
578 nir{ist}=nir{ist}(:);
579 end
580 end
581 SSq(:,samples_collected) = cell2mat(nir');
582
583 %% Update current state and sample counter
584 cur_state_1 = cur_state;
585 cur_state = enabled_next_states{enabled_sync(selected_transition)};
586
587 %% FCR WAITQ bookkeeping (see _kb/06-solver-catalog.md, SSA FCR)
588 if fcrOn && use_inline
589 pk = [0 0 0 0];
590 if selected_transition <= size(enabled_fcr,1)
591 pk = enabled_fcr(selected_transition,:);
592 end
593 if pk(1) > 0 && pk(4) == 0
594 % blocked entry: park the (class, destination) token
595 fcrBuf{pk(1)}(end+1) = (pk(3)-1)*R + pk(2);
596 end
597 % pk(4)==2: DROP, the refused job was destroyed (active part only)
598 % release cascade over all regions
599 [cur_state, fcrBuf, eventCache] = fcr_release(sn, cur_state, fcrBuf, ...
600 fcrMembers, fcrClassCap, fcrGlobalCap, fcrMemCap, fcrSz, fcrA, fcrb, ...
601 isSimulation, eventCache, aectx);
602 if pk(1) > 0 && (pk(4) == 1 || pk(4) == 3)
603 % class-switching hop: gate the re-entry after the cascade
604 f_ = pk(1); cls_ = pk(2); dest_ = pk(3);
605 xf_ = fcr_regionpop(sn, cur_state, fcrMembers{f_});
606 xn_ = xf_; xn_(cls_) = xn_(cls_) + 1;
607 admitted = false;
608 if ~fcr_violates(xn_, fcrClassCap{f_}, fcrGlobalCap(f_), fcrMemCap(f_), fcrSz{f_}, fcrA{f_}, fcrb{f_})
609 isf_d = sn.nodeToStateful(dest_);
610 [ns_, ~, ~, eventCache] = State.afterEvent(sn, dest_, cur_state{isf_d}, EventType.ARV, cls_, isSimulation, eventCache, aectx);
611 if ~isempty(ns_)
612 cur_state{isf_d} = ns_(1,:);
613 admitted = true;
614 end
615 end
616 if ~admitted && pk(4) == 1
617 fcrBuf{f_}(end+1) = (dest_-1)*R + cls_;
618 end
619 % pk(4)==3 refused: DROP, the switching job is destroyed
620 end
621 end
622
623 samples_collected = samples_collected + 1;
624
625 %% Print progress
626 print_progress(options,samples_collected);
627 end
628 % Print newline after progress counter
629 if options.verbose
630 line_printf('\n');
631 end
632catch ME
633 getReport(ME)
634end
635
636% Trim pre-allocated arrays to actual number of samples collected
637samples_collected = samples_collected - 1; % Adjust for the increment at end of loop
638tranState = tranState(:, 1:samples_collected);
639tranSync = tranSync(1:samples_collected, :);
640SSq = SSq(:, 1:samples_collected);
641
642% see _kb/06-solver-catalog.md for rationale (SSA warmup discard)
643warmupfrac = 0.0;
644if isfield(options, 'config') && isfield(options.config, 'warmupfrac')
645 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
646end
647if warmupfrac > 0 && samples_collected > 1
648 nDrop = floor(warmupfrac * samples_collected);
649 if nDrop > 0 && nDrop < samples_collected
650 keep = (nDrop+1):samples_collected;
651 tranState = tranState(:, keep);
652 tranSync = tranSync(keep, :);
653 SSq = SSq(:, keep);
654 if exist('arvRatesSamples', 'var') && ~isempty(arvRatesSamples) ...
655 && size(arvRatesSamples, 1) >= samples_collected
656 arvRatesSamples = arvRatesSamples(keep, :, :);
657 end
658 if exist('depRatesSamples', 'var') && ~isempty(depRatesSamples) ...
659 && size(depRatesSamples, 1) >= samples_collected
660 depRatesSamples = depRatesSamples(keep, :, :);
661 end
662 samples_collected = numel(keep);
663 end
664end
665
666tranState = tranState';
667
668
669[u,ui,uj] = unique(tranState(:,2:end),'rows');
670statesz = cellfun(@length, cur_state_1)';
671tranSysState = cell(1,length(cur_state)+1);
672tranSysState{1} = cumsum(tranState(:,1));
673for j=1:length(statesz)
674 tranSysState{1+j} = tranState(:,1+(1+sum(statesz(1:(j-1)))):(1+sum(statesz(1:j))));
675end
676arvRates = zeros(size(u,1),sn.nstateful,R);
677depRates = zeros(size(u,1),sn.nstateful,R);
678
679pi = zeros(1,size(u,1));
680for s=1:size(u,1)
681 pi(s) = sum(tranState(uj==s,1));
682end
683SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
684
685for ind=1:sn.nnodes
686 if sn.isstateful(ind)
687 isf = sn.nodeToStateful(ind);
688 if sn.isstation(ind)
689 ist = sn.nodeToStation(ind);
690 %K = sn.phasessz(ist,:);
691 %Ks = sn.phaseshift(ist,:);
692 end
693 for s=1:size(u,1)
694 for r=1:R
695 arvRates(s,isf,r) = arvRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate is enough here
696 depRates(s,isf,r) = depRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate is enough here
697 end
698 end
699 end
700end
701pi = pi/sum(pi);
702%sn.nservers = init_nserver; % restore Inf at delay nodes
703end
704
705function print_progress(options,samples_collected)
706if options.verbose && ~batchStartupOptionUsed
707 if samples_collected == 1e2
708 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
709 elseif options.verbose == 2
710 if samples_collected == 0
711 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
712 else
713 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
714 end
715 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
716 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
717 end
718end
719end
720function x = fcr_regionpop(sn, cur_state, members)
721% X=FCR_REGIONPOP(SN,CUR_STATE,MEMBERS) per-class population of a finite
722% capacity region given the current state cells
723x = zeros(1, sn.nclasses);
724for i = members
725 ind_i = sn.stationToNode(i);
726 isf_i = sn.stationToStateful(i);
727 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
728 x = x + nir_i(:)';
729end
730end
731
732function tf = fcr_violates(xn, ccap, gcap, memcap, sz, A, b)
733% TF=FCR_VIOLATES(...) true if population vector xn breaks any admission
734% constraint of the region
735tf = any(xn > ccap) || sum(xn) > gcap || (xn * sz(:) > memcap);
736if ~tf && ~isempty(A)
737 tf = any(A * xn(:) > b(:));
738end
739end
740
741function [cur_state, fcrBuf, eventCache] = fcr_release(sn, cur_state, fcrBuf, ...
742 fcrMembers, fcrClassCap, fcrGlobalCap, fcrMemCap, fcrSz, fcrA, fcrb, ...
743 isSimulation, eventCache, aectx)
744% FCR_RELEASE strict-FIFO head-of-line release of parked region tokens:
745% admit heads while the admission constraints permit, applying the arrival
746% to the destination station state
747K = sn.nclasses;
748progress = true;
749while progress
750 progress = false;
751 for f = 1:length(fcrBuf)
752 if isempty(fcrBuf{f})
753 continue
754 end
755 x = fcr_regionpop(sn, cur_state, fcrMembers{f});
756 tok = fcrBuf{f}(1);
757 dest = floor((tok-1)/K) + 1;
758 r = mod(tok-1, K) + 1;
759 xn = x; xn(r) = xn(r) + 1;
760 if fcr_violates(xn, fcrClassCap{f}, fcrGlobalCap(f), fcrMemCap(f), fcrSz{f}, fcrA{f}, fcrb{f})
761 continue % head-of-line: this region's FIFO stays blocked
762 end
763 isf_d = sn.nodeToStateful(dest);
764 [ns, ~, ~, eventCache] = State.afterEvent(sn, dest, cur_state{isf_d}, EventType.ARV, r, isSimulation, eventCache, aectx);
765 if isempty(ns)
766 continue % destination cannot accept (e.g. station capacity)
767 end
768 cur_state{isf_d} = ns(1,:);
769 fcrBuf{f}(1) = [];
770 progress = true;
771 end
772end
773end
Definition fjtag.m:161