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<DLOBIX>
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 if isinf(sn.nservers(ist))
82 sn.nservers(ist) = sum(capacityc(ind,:));
84 sn.cap(ist,:) = sum(capacityc(ind,:));
85 sn.classcap(ist,:) = capacityc(ind,:);
94init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
97arvRatesSamples = zeros(options.samples,nstateful,R);
98depRatesSamples = zeros(options.samples,nstateful,R);
101samples_collected = 1;
103% fill stateCell with initial states
104cur_state = cell(nstateful,1); % cell array with current stateful node states
106 if sn.isstateful(ind)
107 isf = sn.nodeToStateful(ind);
108 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
110 ist = sn.nodeToStation(ind);
111 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
112 nir{ist} = nir{ist}(:);
116cur_state_1 = cur_state;
117% generate state vector
118state = cell2mat(cur_state
');
119% create function to determine lengths of stateful node states
120statelen = cellfun(@length, cur_state);
121% data structures to save transient information - pre-allocate for all samples
122nSamples = options.samples;
123tranSync = zeros(nSamples,1);
124tranState = zeros(1+length(state), nSamples);
125tranState(1:(1+length(state)),1) = [0, state]';
126SSq = zeros(length(cell2mat(nir
')), nSamples);
127SSq(:,1) = cell2mat(nir');
129last_node_a = 0; % active in the last occurred synchronization
130last_node_p = 0; % passive in the last occurred synchronization
132 node_a{act} = sync{act}.active{1}.node;
133 node_p{act} = sync{act}.passive{1}.node;
134 class_a{act} = sync{act}.active{1}.class;
135 class_p{act} = sync{act}.passive{1}.class;
136 event_a{act} = sync{act}.active{1}.event;
137 event_p{act} = sync{act}.passive{1}.event;
141enabled_next_states = cell(1,A);
143%% Start main simulation loop
144isSimulation =
true; % allow state vector to grow, e.g.
for FCFS buffers
145samples_collected = 1;
147use_inline =
true; %
true = stable version,
false = dev version
150 while samples_collected < options.samples && cur_time <= options.timespan(2)
151 %% This section corresponds to solver_ssa_findenabled in Java
152 %% Inlined
for performance reasons
154 enabled_sync = []; % row
is action label, col1=rate, col2=
new state
160 isf_a = sn.nodeToStateful(node_a{act});
162 enabled_next_states{act} = cur_state;
163 update_cond_a =
true;
165 [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);
168 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
172 for ia=1:size(enabled_next_states{act}{isf_a},1) %
for all possible
new states, check
if they are enabled
173 %
if the transition cannot occur
174 if isnan(rate_a{act}(ia)) || rate_a{act}(ia) == 0 % handles degenerate rate values
175 % set the transition with a zero rate so that it
is
177 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
180 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
183 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});
186 if node_p{act} ~= local
187 if node_p{act} == node_a{act} %self-loop, active and passive are the same
190 [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);
193 isf_p = sn.nodeToStateful(node_p{act});
195 [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);
198 if ~isempty(enabled_next_states{act}{isf_p})
199 if sn.isstatedep(node_a{act},3)
200 prob_sync_p{act} = sync{act}.passive{1}.prob(cur_state, enabled_next_states{act}); %state-dependent
202 prob_sync_p{act} = sync{act}.passive{1}.prob;
205 prob_sync_p{act} = 0;
208 if ~isempty(enabled_next_states{act}{isf_a})
209 if node_p{act} == local
210 prob_sync_p{act} = 1;
212 if ~isnan(rate_a{act})
213 if all(~cellfun(@isempty,enabled_next_states{act}))
214 if event_a{act} == EventType.DEP
215 node_a_sf{act} = isf_a;
216 node_p_sf{act} = isf_p;
217 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};
218 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};
220 % simulate also self-loops as we need to log them
221 %
if any(~cellfun(@isequal,new_state{act},cur_state))
222 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)
223 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}));
225 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
226 enabled_sync(ctr) = act;
236 for gact=1:G %
event at node ind with global side-effects
237 gind = gsync{gact}.active{1}.node; % node index
for global
event
238 [enabled_next_states{A+gact}, outrate, outprob] = State.afterGlobalEvent(sn, gind, cur_state, gsync{gact}, isSimulation);
239 for ia=find(outrate .* outprob)
240 enabled_rates(ctr) = outrate(ia) * outprob(ia);
241 enabled_sync(ctr) = A+gact;
244 % Record departure/arrival rates
for FIRE events at Places
245 if gsync{gact}.active{1}.event == EventType.FIRE
246 mode = gsync{gact}.active{1}.mode;
247 % Get enabling/firing conditions to determine affected
classes
248 enabling_m = sn.nodeparam{gind}.enabling{mode};
249 firing_m = sn.nodeparam{gind}.firing{mode};
251 for j=1:length(gsync{gact}.passive)
252 pev = gsync{gact}.passive{j};
253 % Decode linear index to (node,
class) - pev.node
is a linear index from find() on enabling/firing matrix
254 [pev_node, pev_class] = ind2sub([sn.nnodes, R], pev.node);
255 if pev.event == EventType.PRE
256 % Departure from input Place (consuming tokens)
257 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
258 ep_isf = sn.nodeToStateful(pev_node);
259 % Record departures for the specific class from this PRE event
260 depRatesSamples(samples_collected, ep_isf, pev_class) = ...
261 depRatesSamples(samples_collected, ep_isf, pev_class) + outrate(ia) * outprob(ia);
263 elseif pev.event == EventType.POST
264 % Arrival at output Place (producing tokens)
265 if pev_node <= length(sn.nodeToStateful) && ~isnan(sn.nodeToStateful(pev_node)) && sn.nodeToStateful(pev_node) > 0
266 fp_isf = sn.nodeToStateful(pev_node);
267 % Record arrivals for the specific class from this POST event
268 arvRatesSamples(samples_collected, fp_isf, pev_class) = ...
269 arvRatesSamples(samples_collected, fp_isf, pev_class) + outrate(ia) * outprob(ia);
277 [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);
279 %% Gillespie direct method
280 tot_rate = sum(enabled_rates);
281 cum_rate = cumsum(enabled_rates) / tot_rate;
282 selected_transition = 1 + max([0,find( rand > cum_rate )]); % select action
284 % Update record of last active/passive pair
285 if isempty(enabled_sync)
286 line_error(mfilename,'SSA simulation entered a deadlock before collecting all samples, no synchronization
is enabled.');
288 if selected_transition < gctr_start
290 last_node_a = node_a{enabled_sync(selected_transition)};
291 last_node_p = node_p{enabled_sync(selected_transition)};
298 % This part
is needed to ensure that when the state vector grows the
299 % padding of zero
is done on the left (e.g.,
for FCFS buffers)
302 isf = sn.nodeToStateful(ind);
303 deltalen = length(cur_state{isf}) - statelen(isf);
305 statelen(isf) = length(cur_state{isf});
310 shift = sum(statelen(1:isf-1));
312 pad = zeros(deltalen, size(tranState,2));
313 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
318 %% Simulate the time increment
319 state = cell2mat(cur_state
');
320 dt = -(log(rand)/tot_rate);
321 cur_time = cur_time + dt;
323 %% Save simulation output data
324 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
325 tranSync(samples_collected,1) = enabled_sync(selected_transition);
328 isf = sn.nodeToStateful(ind);
329 ist = sn.nodeToStation(ind);
330 [~,nir{ist}] = State.toMarginal(sn, ind, cur_state{isf});
331 nir{ist}=nir{ist}(:);
334 SSq(:,samples_collected) = cell2mat(nir
');
336 %% Update current state and sample counter
337 cur_state_1 = cur_state;
338 cur_state = enabled_next_states{enabled_sync(selected_transition)};
340 samples_collected = samples_collected + 1;
343 print_progress(options,samples_collected);
345 % Print newline after progress counter
353% Trim pre-allocated arrays to actual number of samples collected
354samples_collected = samples_collected - 1; % Adjust for the increment at end of loop
355tranState = tranState(:, 1:samples_collected);
356tranSync = tranSync(1:samples_collected, :);
357SSq = SSq(:, 1:samples_collected);
359%transient = min([floor(samples_collected/10),1000]); % remove first part of simulation (10% of the samples up to 1000 max)
361%output = output((transient+1):end,:);
362tranState = tranState';
365[u,ui,uj] = unique(tranState(:,2:end),
'rows');
366statesz = cellfun(@length, cur_state_1)
';
367tranSysState = cell(1,length(cur_state)+1);
368tranSysState{1} = cumsum(tranState(:,1));
369for j=1:length(statesz)
370 tranSysState{1+j} = tranState(:,1+(1+sum(statesz(1:(j-1)))):(1+sum(statesz(1:j))));
372arvRates = zeros(size(u,1),sn.nstateful,R);
373depRates = zeros(size(u,1),sn.nstateful,R);
375pi = zeros(1,size(u,1));
377 pi(s) = sum(tranState(uj==s,1));
379SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
382 if sn.isstateful(ind)
383 isf = sn.nodeToStateful(ind);
385 ist = sn.nodeToStation(ind);
386 %K = sn.phasessz(ist,:);
387 %Ks = sn.phaseshift(ist,:);
391 arvRates(s,isf,r) = arvRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate
is enough here
392 depRates(s,isf,r) = depRatesSamples(ui(s),isf,r); % for each unique state, one (any) sample of the rate
is enough here
398%sn.nservers = init_nserver; % restore Inf at delay
nodes
401function print_progress(options,samples_collected)
403 if samples_collected == 1e2
404 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
405 elseif options.verbose == 2
406 if samples_collected == 0
407 line_printf(sprintf('\nSSA samples: %6d',samples_collected));
409 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
411 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
412 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));