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
9if ~isfield(options,
'seed')
12% Handle parallel computing toolbox gracefully - get worker index
13if isMATLABReleaseOlderThan("R2022b")
14 % Use labindex for older MATLAB versions
16 if ~isempty(getCurrentTask())
17 lab_idx = labindex(); %
#ok<DLABINDEX>
22 line_warning(mfilename,
'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
26 % Use spmdIndex
for R2022b and newer (labindex
is deprecated)
29 if isempty(lab_idx) || lab_idx == 0
33 line_warning(mfilename,
'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
37Solver.resetRandomGeneratorSeed(options.seed + lab_idx - 1);
39%% generate local state spaces
40%nstations = sn.nstations;
41nstateful = sn.nstateful;
42%init_nserver = sn.nservers; % restore Inf at delay
nodes
49line_debug('SSA solver starting: nstateful=%d, nclasses=%d, njobs=%s, samples=%d
', nstateful, R, mat2str(N), options.samples);
52cutoff = options.cutoff;
54 cutoff = cutoff * ones(sn.nstations, sn.nclasses);
59capacityc = zeros(sn.nnodes, sn.nclasses);
60original_classcap = sn.classcap; % preserve original classcap
for class switching scenarios
62 if sn.isstation(ind) % place jobs across stations
63 ist = sn.nodeToStation(ind);
64 %isf = sn.nodeToStateful(ind);
65 for r=1:sn.nclasses %cut-off open
classes to finite capacity
66 c = find(sn.chains(:,r));
68 % receive jobs via
class switching (indicated by non-zero original classcap)
69 if ~isempty(sn.
visits{c}) && sn.visits{c}(ist,r) == 0 && original_classcap(ist,r) == 0
71 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)
75 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
77 capacityc(ind,r) = sum(sn.njobs(sn.chains(c,:)));
81 capacity_sum = sum(capacityc(ind,:));
82 if sn.sched(ist) == SchedStrategy.PAS
83 % Pass-and-swap stations are a single order-independent buffer of
84 % total size cap, not a sum of per-
class buffers. Preserve the
85 % original total capacity, which
is also the physical width used to
86 % size the ordered-list state (State.fromMarginal Wpas = cap).
87 capacity_sum = sn.cap(ist);
89 if isinf(sn.nservers(ist))
90 sn.nservers(ist) = capacity_sum;
92 sn.cap(ist,:) = capacity_sum;
93 sn.classcap(ist,:) = capacityc(ind,:);
102init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
105arvRatesSamples = zeros(options.samples,nstateful,R);
106depRatesSamples = zeros(options.samples,nstateful,R);
109samples_collected = 1;
111% fill stateCell with initial states
112cur_state = cell(nstateful,1); % cell
array with current stateful node states
114 if sn.isstateful(ind)
115 isf = sn.nodeToStateful(ind);
116 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
118 ist = sn.nodeToStation(ind);
119 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
120 nir{ist} = nir{ist}(:);
124cur_state_1 = cur_state;
125% generate state vector
126state = cell2mat(cur_state
');
127% create function to determine lengths of stateful node states
128statelen = cellfun(@length, cur_state);
129% data structures to save transient information - pre-allocate for all samples
130nSamples = options.samples;
131tranSync = zeros(nSamples,1);
132tranState = zeros(1+length(state), nSamples);
133tranState(1:(1+length(state)),1) = [0, state]';
134SSq = zeros(length(cell2mat(nir
')), nSamples);
135SSq(:,1) = cell2mat(nir');
137last_node_a = 0; % active in the last occurred synchronization
138last_node_p = 0; % passive in the last occurred synchronization
140 node_a{act} = sync{act}.active{1}.node;
141 node_p{act} = sync{act}.passive{1}.node;
142 class_a{act} = sync{act}.active{1}.class;
143 class_p{act} = sync{act}.passive{1}.class;
144 event_a{act} = sync{act}.active{1}.event;
145 event_p{act} = sync{act}.passive{1}.event;
149enabled_next_states = cell(1,A);
151%% Start main simulation loop
152isSimulation =
true; % allow state vector to grow, e.g.
for FCFS buffers
153samples_collected = 1;
155use_inline =
true; %
true = stable version,
false = dev version
158 while samples_collected < options.samples && cur_time <= options.timespan(2)
159 %% This section corresponds to solver_ssa_findenabled in Java
160 %% Inlined
for performance reasons
162 enabled_sync = []; % row
is action label, col1=rate, col2=
new state
168 isf_a = sn.nodeToStateful(node_a{act});
170 enabled_next_states{act} = cur_state;
171 update_cond_a =
true;
173 [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);
176 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
180 for ia=1:size(enabled_next_states{act}{isf_a},1) %
for all possible
new states, check
if they are enabled
181 %
if the transition cannot occur
182 if isnan(rate_a{act}(ia)) || rate_a{act}(ia) == 0 % handles degenerate rate values
183 % set the transition with a zero rate so that it
is
185 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
188 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
191 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});
194 if node_p{act} ~= local
195 if node_p{act} == node_a{act} %self-loop, active and passive are the same
198 [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);
201 isf_p = sn.nodeToStateful(node_p{act});
203 [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);
206 if ~isempty(enabled_next_states{act}{isf_p})
207 if sn.isstatedep(node_a{act},3)
208 prob_sync_p{act} = sync{act}.passive{1}.prob(cur_state, enabled_next_states{act}); %state-dependent
210 prob_sync_p{act} = sync{act}.passive{1}.prob;
213 prob_sync_p{act} = 0;
216 if ~isempty(enabled_next_states{act}{isf_a})
217 if node_p{act} == local
218 prob_sync_p{act} = 1;
220 if ~isnan(rate_a{act})
221 if all(~cellfun(@isempty,enabled_next_states{act}))
222 if event_a{act} == EventType.DEP
223 node_a_sf{act} = isf_a;
224 node_p_sf{act} = isf_p;
225 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};
226 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};
228 % simulate also self-loops as we need to log them
229 %
if any(~cellfun(@isequal,new_state{act},cur_state))
230 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)
231 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}));
233 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
234 enabled_sync(ctr) = act;
244 for gact=1:G %
event at node ind with global side-effects
245 gind = gsync{gact}.active{1}.node; % node index
for global
event
246 [enabled_next_states{A+gact}, outrate, outprob] = State.afterGlobalEvent(sn, gind, cur_state, gsync{gact}, isSimulation);
247 for ia=find(outrate .* outprob)
248 enabled_rates(ctr) = outrate(ia) * outprob(ia);
249 enabled_sync(ctr) = A+gact;
252 % Record departure/arrival rates
for FIRE events at Places
253 if gsync{gact}.active{1}.event == EventType.FIRE
254 mode = gsync{gact}.active{1}.mode;
255 % Get enabling/firing conditions to determine affected
classes
256 enabling_m = sn.nodeparam{gind}.enabling{mode};
257 firing_m = sn.nodeparam{gind}.firing{mode};
259 for j=1:length(gsync{gact}.passive)
260 pev = gsync{gact}.passive{j};
261 % Decode linear index to (node,
class) - pev.node
is a linear index from find() on enabling/firing matrix
262 [pev_node, pev_class] = ind2sub([sn.nnodes, R], pev.node);
263 if pev.event == EventType.PRE
264 % Departure from input Place (consuming tokens)
265 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
266 ep_isf = sn.nodeToStateful(pev_node);
267 % Record departures for the specific class from this PRE event
268 depRatesSamples(samples_collected, ep_isf, pev_class) = ...
269 depRatesSamples(samples_collected, ep_isf, pev_class) + outrate(ia) * outprob(ia);
271 elseif pev.event == EventType.POST
272 % Arrival at output Place (producing tokens)
273 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
274 fp_isf = sn.nodeToStateful(pev_node);
275 % Record arrivals for the specific class from this POST event
276 arvRatesSamples(samples_collected, fp_isf, pev_class) = ...
277 arvRatesSamples(samples_collected, fp_isf, pev_class) + outrate(ia) * outprob(ia);
285 [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);
287 %% Gillespie direct method
288 tot_rate = sum(enabled_rates);
289 cum_rate = cumsum(enabled_rates) / tot_rate;
290 selected_transition = 1 + max([0,find( rand > cum_rate )]); % select action
292 % Update record of last active/passive pair
293 if isempty(enabled_sync)
294 line_error(mfilename,'SSA simulation entered a deadlock before collecting all samples, no synchronization
is enabled.');
296 if selected_transition < gctr_start
298 last_node_a = node_a{enabled_sync(selected_transition)};
299 last_node_p = node_p{enabled_sync(selected_transition)};
306 % This part
is needed to ensure that when the state vector grows the
307 % padding of zero
is done on the left (e.g.,
for FCFS buffers)
310 isf = sn.nodeToStateful(ind);
311 deltalen = length(cur_state{isf}) - statelen(isf);
313 statelen(isf) = length(cur_state{isf});
318 shift = sum(statelen(1:isf-1));
320 pad = zeros(deltalen, size(tranState,2));
321 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
326 %% Simulate the time increment
327 state = cell2mat(cur_state
');
328 dt = -(log(rand)/tot_rate);
329 cur_time = cur_time + dt;
331 %% Save simulation output data
332 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
333 tranSync(samples_collected,1) = enabled_sync(selected_transition);
336 isf = sn.nodeToStateful(ind);
337 ist = sn.nodeToStation(ind);
338 [~,nir{ist}] = State.toMarginal(sn, ind, cur_state{isf});
339 nir{ist}=nir{ist}(:);
342 SSq(:,samples_collected) = cell2mat(nir
');
344 %% Update current state and sample counter
345 cur_state_1 = cur_state;
346 cur_state = enabled_next_states{enabled_sync(selected_transition)};
348 % KCHOICES with memory: record the destination just chosen so the
349 % next dispatch consults it. Only applies to Router-typed active
350 % nodes that have KCHOICES routing with withMemory=true. This is the
351 % MATLAB-side mirror of LDES's kchoicesLastSelected tracking.
352 if selected_transition < gctr_start
353 actSync = enabled_sync(selected_transition);
354 if ~isempty(node_a{actSync}) && ~isempty(node_p{actSync}) ...
355 && node_a{actSync} >= 1 && node_a{actSync} <= sn.nnodes
356 aNode = node_a{actSync};
357 aClass = class_a{actSync};
358 if sn.nodetype(aNode) == NodeType.Router && aClass >= 1 ...
359 && sn.routing(aNode, aClass) == RoutingStrategy.KCHOICES
361 if iscell(sn.nodeparam) && aNode <= numel(sn.nodeparam) ...
362 && iscell(sn.nodeparam{aNode}) ...
363 && aClass <= numel(sn.nodeparam{aNode})
364 np_a = sn.nodeparam{aNode}{aClass};
366 if ~isempty(np_a) && isfield(np_a,
'withMemory') && np_a.withMemory
367 isf_a = sn.nodeToStateful(aNode);
369 if ~isempty(cur_state{isf_a}) && length(cur_state{isf_a}) >= Rcl
370 cur_state{isf_a}(end - Rcl + aClass) = node_p{actSync};
377 samples_collected = samples_collected + 1;
380 print_progress(options,samples_collected);
382 % Print newline after progress counter
390% Trim pre-allocated arrays to actual number of samples collected
391samples_collected = samples_collected - 1; % Adjust
for the increment at end of loop
392tranState = tranState(:, 1:samples_collected);
393tranSync = tranSync(1:samples_collected, :);
394SSq = SSq(:, 1:samples_collected);
396% Warmup discard: drop the first floor(warmupfrac * samples) samples so that
397% steady-state averages are not biased by transient behaviour. Controlled by
398% options.config.warmupfrac (
default 0.0 = no discard).
400if isfield(options,
'config') && isfield(options.config,
'warmupfrac')
401 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
403if warmupfrac > 0 && samples_collected > 1
404 nDrop = floor(warmupfrac * samples_collected);
405 if nDrop > 0 && nDrop < samples_collected
406 keep = (nDrop+1):samples_collected;
407 tranState = tranState(:, keep);
408 tranSync = tranSync(keep, :);
410 if exist('arvRatesSamples', 'var') && ~isempty(arvRatesSamples) ...
411 && size(arvRatesSamples, 1) >= samples_collected
412 arvRatesSamples = arvRatesSamples(keep, :, :);
414 if exist('depRatesSamples', 'var') && ~isempty(depRatesSamples) ...
415 && size(depRatesSamples, 1) >= samples_collected
416 depRatesSamples = depRatesSamples(keep, :, :);
418 samples_collected = numel(keep);
422tranState = tranState';
425[u,ui,uj] = unique(tranState(:,2:end),'rows');
426statesz = cellfun(@length, cur_state_1)';
427tranSysState = cell(1,length(cur_state)+1);
428tranSysState{1} = cumsum(tranState(:,1));
429for j=1:length(statesz)
430 tranSysState{1+j} = tranState(:,1+(1+sum(statesz(1:(j-1)))):(1+sum(statesz(1:j))));
432arvRates = zeros(size(u,1),sn.nstateful,R);
433depRates = zeros(size(u,1),sn.nstateful,R);
435pi = zeros(1,size(u,1));
437 pi(s) = sum(tranState(uj==s,1));
439SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
442 if sn.isstateful(ind)
443 isf = sn.nodeToStateful(ind);
445 ist = sn.nodeToStation(ind);
446 %K = sn.phasessz(ist,:);
447 %Ks = sn.phaseshift(ist,:);
451 arvRates(s,isf,r) = arvRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate
is enough here
452 depRates(s,isf,r) = depRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate
is enough here
458%sn.nservers = init_nserver; % restore Inf at delay
nodes
461function print_progress(options,samples_collected)
463 if samples_collected == 1e2
464 line_printf(sprintf('\bSSA samples: %6d',samples_collected));
465 elseif options.verbose == 2
466 if samples_collected == 0
467 line_printf(sprintf('\bSSA samples: %6d',samples_collected));
469 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
471 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
472 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));