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 % 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;
117 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
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));
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);
134 if isinf(sn.nservers(ist))
135 sn.nservers(ist) = capacity_sum;
137 sn.cap(ist,:) = capacity_sum;
138 sn.classcap(ist,:) = capacityc(ind,:);
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;
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).
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};
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>
168 line_error(mfilename,
'SolverSSA supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
170 if numel(served) == 1
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>
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));
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));
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;
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}(:);
218 mask = (any(Rmat ~= -1, 2) | memvecFCR ~= -1)
';
219 fcrMemberMask{f} = mask;
220 fcrMembers{f} = find(mask);
223 cv = Rmat(fcrMembers{f}, r); cv = cv(cv ~= -1);
224 if ~isempty(cv); ccap(r) = min(cv); 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
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};
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
246 fcrRule(f,r) = sn.regionrule(f,r) ~= DropStrategy.DROP;
250 fcrBuf = cell(sn.nregions,1);
251 for f = 1:sn.nregions
252 fcrBuf{f} = zeros(1,0);
261init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
264arvRatesSamples = zeros(options.samples,nstateful,R);
265depRatesSamples = zeros(options.samples,nstateful,R);
268samples_collected = 1;
270% fill stateCell with initial states
271cur_state = cell(nstateful,1); % cell array with current stateful node states
273 if sn.isstateful(ind)
274 isf = sn.nodeToStateful(ind);
275 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
277 ist = sn.nodeToStation(ind);
278 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
279 nir{ist} = nir{ist}(:);
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
');
296last_node_a = 0; % active in the last occurred synchronization
297last_node_p = 0; % passive in the last occurred synchronization
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;
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;
325enabled_next_states = cell(1,A);
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;
336use_inline = true; % true = stable version, false = dev version
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
343 enabled_sync = []; % row is action label, col1=rate, col2=new state
345 enabled_fcr = zeros(0,4); % [region class dest isSwitch] FCR marker per transition
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.
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});
365 isf_a = sn.nodeToStateful(node_a{act});
367 enabled_next_states{act} = cur_state;
368 update_cond_a =
true;
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});
373 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
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
382 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
385 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
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});
391 if node_p{act} ~= local
392 if node_p{act} == node_a{act} %self-loop, active and passive are
the same
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);
398 isf_p = sn.nodeToStateful(node_p{act});
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);
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
407 prob_sync_p{act} = sync{act}.passive{1}.prob;
410 prob_sync_p{act} = 0;
413 if ~isempty(enabled_next_states{act}{isf_a})
414 if node_p{act} == local
415 prob_sync_p{act} = 1;
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).
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});
431 ja = sn.nodeToStation(node_a{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}(:)))
441 fcrMark = [f cc node_p{act} 0]; % park in FIFO
443 fcrMark = [f cc node_p{act} 2]; % DROP: destroyed
447 elseif mmask(jp) && ja > 0 && ja <= numel(mmask) && mmask(ja) ...
448 && cc ~= class_a{act}
450 fcrMark = [f cc node_p{act} 1]; % exit + gated re-entry
452 fcrMark = [f cc node_p{act} 3]; % exit + gated re-entry, DROP on refusal
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};
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};
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}));
477 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
478 enabled_sync(ctr) = act;
479 enabled_fcr(ctr,:) = fcrMark;
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;
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};
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);
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);
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
536 if isfield(sn,'fjsync
') && ~isempty(sn.fjsync)
537 FJ = length(sn.fjsync);
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;
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);
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);
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
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.
');
569 if selected_transition < gctr_start
571 last_node_a = node_a{enabled_sync(selected_transition)};
572 last_node_p = node_p{enabled_sync(selected_transition)};
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)
583 isf = sn.nodeToStateful(ind);
584 deltalen = length(cur_state{isf}) - statelen(isf);
586 statelen(isf) = length(cur_state{isf});
591 shift = sum(statelen(1:isf-1));
593 pad = zeros(deltalen, size(tranState,2));
594 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
599 %% Simulate the time increment
600 state = cell2mat(cur_state');
601 dt = -(log(rand)/tot_rate);
602 cur_time = cur_time + dt;
604 %% Save simulation output data
605 tranState(1:(1+length(state)),samples_collected) = [dt, state]
';
606 tranSync(samples_collected,1) = enabled_sync(selected_transition);
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}(:);
615 SSq(:,samples_collected) = cell2mat(nir');
617 %% Update current state and sample counter
618 cur_state_1 = cur_state;
619 cur_state = enabled_next_states{enabled_sync(selected_transition)};
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
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};
639 if ~isempty(np_a) && isfield(np_a, 'withMemory
') && np_a.withMemory
640 isf_a = sn.nodeToStateful(aNode);
642 if ~isempty(cur_state{isf_a}) && length(cur_state{isf_a}) >= Rcl
643 cur_state{isf_a}(end - Rcl + aClass) = node_p{actSync};
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
655 if selected_transition <= size(enabled_fcr,1)
656 pk = enabled_fcr(selected_transition,:);
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);
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;
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);
677 cur_state{isf_d} = ns_(1,:);
681 if ~admitted && pk(4) == 1
682 fcrBuf{f_}(end+1) = (dest_-1)*R + cls_;
684 % pk(4)==3 refused: DROP, the switching job is destroyed
688 samples_collected = samples_collected + 1;
691 print_progress(options,samples_collected);
693 % Print newline after progress counter
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);
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).
711if isfield(options, 'config
') && isfield(options.config, 'warmupfrac
')
712 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
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, :);
721 if exist('arvRatesSamples
', 'var
') && ~isempty(arvRatesSamples) ...
722 && size(arvRatesSamples, 1) >= samples_collected
723 arvRatesSamples = arvRatesSamples(keep, :, :);
725 if exist('depRatesSamples
', 'var
') && ~isempty(depRatesSamples) ...
726 && size(depRatesSamples, 1) >= samples_collected
727 depRatesSamples = depRatesSamples(keep, :, :);
729 samples_collected = numel(keep);
733tranState = tranState';
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))));
743arvRates = zeros(size(u,1),sn.nstateful,R);
744depRates = zeros(size(u,1),sn.nstateful,R);
746pi = zeros(1,size(u,1));
748 pi(s) = sum(tranState(uj==s,1));
750SSq = SSq(:,ui)'; % we restrict to unique states in
the simulation
753 if sn.isstateful(ind)
754 isf = sn.nodeToStateful(ind);
756 ist = sn.nodeToStation(ind);
757 %K = sn.phasessz(ist,:);
758 %Ks = sn.phaseshift(ist,:);
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
769%sn.nservers = init_nserver; % restore Inf at delay
nodes
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));
780 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
782 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
783 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
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);
792 ind_i = sn.stationToNode(i);
793 isf_i = sn.stationToStateful(i);
794 [~, nir_i] = State.toMarginalAggr(sn, ind_i, cur_state{isf_i});
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);
804 tf = any(A * xn(:) > b(:));
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
818 for f = 1:length(fcrBuf)
819 if isempty(fcrBuf{f})
822 x = fcr_regionpop(sn, cur_state, fcrMembers{f});
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
830 isf_d = sn.nodeToStateful(dest);
831 [ns, ~, ~, eventCache] = State.afterEvent(sn, dest, cur_state{isf_d}, EventType.ARV, r, isSimulation, eventCache, aectx);
833 continue % destination cannot accept (e.g. station capacity)
835 cur_state{isf_d} = ns(1,:);