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)
4% Copyright (c) 2012-2026, Imperial College London
7% by
default the jobs are all initialized in the first valid state
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);
14 line_error(mfilename,'SolverSSA supports only exponential (memoryless) patience for reneging. Use SolverLDES or SolverJMT for phase-type patience.');
17if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy)
18 badBalk = (sn.balkingStrategy~=0) & (sn.balkingStrategy~=BalkingStrategy.QUEUE_LENGTH);
20 line_error(mfilename,'SolverSSA supports only QUEUE_LENGTH balking. Use SolverLDES or SolverJMT for wait-time-based balking.');
23if isfield(sn,'retrialProc') && ~isempty(sn.retrialProc)
24 hasRetrial = ~cellfun(@isempty, sn.retrialProc);
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.');
29 if any(hasRetrial(:) & (sn.retrialMaxAttempts(:)>=0))
30 line_error(mfilename,'SolverSSA supports only unlimited retrials (maxAttempts=-1). Use SolverLDES for finite max-attempts.');
32 for ii = find(any(hasRetrial,2))'
34 for rr = 1:sn.nclasses
35 if ~isempty(sn.proc{ii}{rr}) && ~any(any(isnan(sn.proc{ii}{rr}{1})))
40 line_error(mfilename,
'SolverSSA supports retrial only for single-class stations. Use SolverLDES for multi-class retrial.');
46if ~isfield(options,
'seed')
49% Handle parallel computing toolbox gracefully - get worker index
50if isMATLABReleaseOlderThan("R2022b")
51 % Use labindex for older MATLAB versions
53 if ~isempty(getCurrentTask())
54 lab_idx = labindex(); %
#ok<DLABINDEX>
59 line_warning(mfilename,
'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
63 % Use spmdIndex
for R2022b and newer (labindex
is deprecated)
66 if isempty(lab_idx) || lab_idx == 0
70 line_warning(mfilename,
'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
74Solver.resetRandomGeneratorSeed(options.seed + lab_idx - 1);
76%% generate local state spaces
77%nstations = sn.nstations;
78nstateful = sn.nstateful;
79%init_nserver = sn.nservers; % restore Inf at delay
nodes
86line_debug('SSA solver starting: nstateful=%d, nclasses=%d, njobs=%s, samples=%d
', nstateful, R, mat2str(N), options.samples);
89cutoff = options.cutoff;
91 cutoff = cutoff * ones(sn.nstations, sn.nclasses);
96capacityc = zeros(sn.nnodes, sn.nclasses);
97original_classcap = sn.classcap; % preserve original classcap
for class switching scenarios
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;
115 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
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));
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);
129 if isinf(sn.nservers(ist))
130 sn.nservers(ist) = capacity_sum;
132 sn.cap(ist,:) = capacity_sum;
133 sn.classcap(ist,:) = capacityc(ind,:);
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;
145% see _kb/06-solver-catalog.md for rationale (SSA heterogeneous servers)
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};
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>
159 line_error(mfilename,
'SolverSSA supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
161 if numel(served) == 1
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>
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));
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));
186% see _kb/06-solver-catalog.md for rationale (SSA FCR)
187fcrOn = isfield(sn,'nregions') && sn.nregions > 0;
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}(:);
206 mask = (any(Rmat ~= -1, 2) | memvecFCR ~= -1)
';
207 fcrMemberMask{f} = mask;
208 fcrMembers{f} = find(mask);
211 cv = Rmat(fcrMembers{f}, r); cv = cv(cv ~= -1);
212 if ~isempty(cv); ccap(r) = min(cv); 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
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};
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
232 fcrRule(f,r) = sn.regionrule(f,r) ~= DropStrategy.DROP;
236 fcrBuf = cell(sn.nregions,1);
237 for f = 1:sn.nregions
238 fcrBuf{f} = zeros(1,0);
247init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
250arvRatesSamples = zeros(options.samples,nstateful,R);
251depRatesSamples = zeros(options.samples,nstateful,R);
254samples_collected = 1;
256% fill stateCell with initial states
257cur_state = cell(nstateful,1); % cell array with current stateful node states
259 if sn.isstateful(ind)
260 isf = sn.nodeToStateful(ind);
261 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
263 ist = sn.nodeToStation(ind);
264 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
265 nir{ist} = nir{ist}(:);
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
');
282last_node_a = 0; % active in the last occurred synchronization
283last_node_p = 0; % passive in the last occurred synchronization
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;
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;
306enabled_next_states = cell(1,A);
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;
314use_inline = true; % true = stable version, false = dev version
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
321 enabled_sync = []; % row is action label, col1=rate, col2=new state
323 enabled_fcr = zeros(0,4); % [region class dest isSwitch] FCR marker per transition
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.
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});
343 isf_a = sn.nodeToStateful(node_a{act});
345 enabled_next_states{act} = cur_state;
346 update_cond_a =
true;
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});
351 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
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
360 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
363 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
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});
369 if node_p{act} ~= local
370 if node_p{act} == node_a{act} %self-loop, active and passive are the same
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);
376 isf_p = sn.nodeToStateful(node_p{act});
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);
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
385 prob_sync_p{act} = sync{act}.passive{1}.prob;
388 prob_sync_p{act} = 0;
391 if ~isempty(enabled_next_states{act}{isf_a})
392 if node_p{act} == local
393 prob_sync_p{act} = 1;
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)
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});
403 ja = sn.nodeToStation(node_a{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}(:)))
413 fcrMark = [f cc node_p{act} 0]; % park in FIFO
415 fcrMark = [f cc node_p{act} 2]; % DROP: destroyed
419 elseif mmask(jp) && ja > 0 && ja <= numel(mmask) && mmask(ja) ...
420 && cc ~= class_a{act}
422 fcrMark = [f cc node_p{act} 1]; % exit + gated re-entry
424 fcrMark = [f cc node_p{act} 3]; % exit + gated re-entry, DROP on refusal
432 % see _kb/06-solver-catalog.md
for rationale (SSA FCR)
433 enabled_next_states{act}{isf_p} = cur_state{isf_p};
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};
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}));
447 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
448 enabled_sync(ctr) = act;
449 enabled_fcr(ctr,:) = fcrMark;
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;
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};
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);
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);
501 % see _kb/06-solver-catalog.md for rationale (native fork-join)
503 if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
504 FJ = length(sn.fjsync);
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;
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);
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);
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
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.');
536 if selected_transition < gctr_start
538 last_node_a = node_a{enabled_sync(selected_transition)};
539 last_node_p = node_p{enabled_sync(selected_transition)};
546 % see _kb/06-solver-catalog.md
for rationale (SSA state left-padding)
549 isf = sn.nodeToStateful(ind);
550 deltalen = length(cur_state{isf}) - statelen(isf);
552 statelen(isf) = length(cur_state{isf});
557 shift = sum(statelen(1:isf-1));
559 pad = zeros(deltalen, size(tranState,2));
560 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
565 %% Simulate the time increment
566 state = cell2mat(cur_state
');
567 dt = -(log(rand)/tot_rate);
568 cur_time = cur_time + dt;
570 %% Save simulation output data
571 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
572 tranSync(samples_collected,1) = enabled_sync(selected_transition);
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}(:);
581 SSq(:,samples_collected) = cell2mat(nir
');
583 %% Update current state and sample counter
584 cur_state_1 = cur_state;
585 cur_state = enabled_next_states{enabled_sync(selected_transition)};
587 %% FCR WAITQ bookkeeping (see _kb/06-solver-catalog.md, SSA FCR)
588 if fcrOn && use_inline
590 if selected_transition <= size(enabled_fcr,1)
591 pk = enabled_fcr(selected_transition,:);
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);
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;
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);
612 cur_state{isf_d} = ns_(1,:);
616 if ~admitted && pk(4) == 1
617 fcrBuf{f_}(end+1) = (dest_-1)*R + cls_;
619 % pk(4)==3 refused: DROP, the switching job is destroyed
623 samples_collected = samples_collected + 1;
626 print_progress(options,samples_collected);
628 % Print newline after progress counter
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);
642% see _kb/06-solver-catalog.md for rationale (SSA warmup discard)
644if isfield(options, 'config
') && isfield(options.config, 'warmupfrac
')
645 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
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, :);
654 if exist('arvRatesSamples
', 'var
') && ~isempty(arvRatesSamples) ...
655 && size(arvRatesSamples, 1) >= samples_collected
656 arvRatesSamples = arvRatesSamples(keep, :, :);
658 if exist('depRatesSamples
', 'var
') && ~isempty(depRatesSamples) ...
659 && size(depRatesSamples, 1) >= samples_collected
660 depRatesSamples = depRatesSamples(keep, :, :);
662 samples_collected = numel(keep);
666tranState = tranState';
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))));
676arvRates = zeros(size(u,1),sn.nstateful,R);
677depRates = zeros(size(u,1),sn.nstateful,R);
679pi = zeros(1,size(u,1));
681 pi(s) = sum(tranState(uj==s,1));
683SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
686 if sn.isstateful(ind)
687 isf = sn.nodeToStateful(ind);
689 ist = sn.nodeToStation(ind);
690 %K = sn.phasessz(ist,:);
691 %Ks = sn.phaseshift(ist,:);
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
702%sn.nservers = init_nserver; % restore Inf at delay
nodes
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));
713 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
715 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
716 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
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);
725 ind_i = sn.stationToNode(i);
726 isf_i = sn.stationToStateful(i);
727 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
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);
737 tf = any(A * xn(:) > b(:));
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
751 for f = 1:length(fcrBuf)
752 if isempty(fcrBuf{f})
755 x = fcr_regionpop(sn, cur_state, fcrMembers{f});
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
763 isf_d = sn.nodeToStateful(dest);
764 [ns, ~, ~, eventCache] = State.afterEvent(sn, dest, cur_state{isf_d}, EventType.ARV, r, isSimulation, eventCache, aectx);
766 continue % destination cannot accept (e.g. station capacity)
768 cur_state{isf_d} = ns(1,:);