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
9if ~isfield(options,'seed')
10 options.seed = 23000;
11end
12% Handle parallel computing toolbox gracefully - get worker index
13if isMATLABReleaseOlderThan("R2022b")
14 % Use labindex for older MATLAB versions
15 try
16 if ~isempty(getCurrentTask())
17 lab_idx = labindex(); %#ok<DLOBIX>
18 else
19 lab_idx = 1;
20 end
21 catch
22 line_warning(mfilename,'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
23 lab_idx = 1;
24 end
25else
26 % Use spmdIndex for R2022b and newer (labindex is deprecated)
27 try
28 lab_idx = spmdIndex;
29 if isempty(lab_idx) || lab_idx == 0
30 lab_idx = 1;
31 end
32 catch
33 line_warning(mfilename,'Parallel Computing Toolbox not available or not running in parallel mode. Using labindex = 1.');
34 lab_idx = 1;
35 end
36end
37Solver.resetRandomGeneratorSeed(options.seed + lab_idx - 1);
38
39%% generate local state spaces
40%nstations = sn.nstations;
41nstateful = sn.nstateful;
42%init_nserver = sn.nservers; % restore Inf at delay nodes
43R = sn.nclasses;
44N = sn.njobs';
45nnodes = sn.nnodes;
46sync = sn.sync;
47gsync = sn.gsync;
48
49line_debug('SSA solver starting: nstateful=%d, nclasses=%d, njobs=%s, samples=%d', nstateful, R, mat2str(N), options.samples);
50csmask = sn.csmask;
51
52cutoff = options.cutoff;
53if isscalar(cutoff)
54 cutoff = cutoff * ones(sn.nstations, sn.nclasses);
55end
56
57%%
58Np = N';
59capacityc = zeros(sn.nnodes, sn.nclasses);
60original_classcap = sn.classcap; % preserve original classcap for class switching scenarios
61for ind=1:sn.nnodes
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));
67 % Check if visits is 0, but also preserve capacity for classes that can
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
70 capacityc(ind,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)
72 capacityc(ind,r) = 0;
73 else
74 if isinf(N(r))
75 capacityc(ind,r) = min(cutoff(ist,r), sn.classcap(ist,r));
76 else
77 capacityc(ind,r) = sum(sn.njobs(sn.chains(c,:)));
78 end
79 end
80 end
81 if isinf(sn.nservers(ist))
82 sn.nservers(ist) = sum(capacityc(ind,:));
83 end
84 sn.cap(ist,:) = sum(capacityc(ind,:));
85 sn.classcap(ist,:) = capacityc(ind,:);
86 end
87end
88
89%%
90if any(isinf(Np))
91 Np(isinf(Np)) = 0;
92end
93
94init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
95
96%%
97arvRatesSamples = zeros(options.samples,nstateful,R);
98depRatesSamples = zeros(options.samples,nstateful,R);
99A = length(sync);
100G = length(gsync);
101samples_collected = 1;
102nir = {};
103% fill stateCell with initial states
104cur_state = cell(nstateful,1); % cell array with current stateful node states
105for ind=1:sn.nnodes
106 if sn.isstateful(ind)
107 isf = sn.nodeToStateful(ind);
108 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
109 if sn.isstation(ind)
110 ist = sn.nodeToStation(ind);
111 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
112 nir{ist} = nir{ist}(:);
113 end
114 end
115end
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');
128local = sn.nnodes+1;
129last_node_a = 0; % active in the last occurred synchronization
130last_node_p = 0; % passive in the last occurred synchronization
131for act=1:A
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;
138 outprob_a{act} = [];
139 outprob_p{act} = [];
140end
141enabled_next_states = cell(1,A);
142
143%% Start main simulation loop
144isSimulation = true; % allow state vector to grow, e.g. for FCFS buffers
145samples_collected = 1;
146cur_time = 0;
147use_inline = true; % true = stable version, false = dev version
148
149try
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
153 if use_inline
154 enabled_sync = []; % row is action label, col1=rate, col2=new state
155 enabled_rates = [];
156 ctr = 1;
157 A = length(sync);
158 G = length(gsync);
159 for act=1:A
160 isf_a = sn.nodeToStateful(node_a{act});
161
162 enabled_next_states{act} = cur_state;
163 update_cond_a = true;
164 if update_cond_a
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);
166 end
167
168 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
169 continue
170 end
171
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
176 % never selected
177 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
178 end
179
180 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
181 continue
182 end
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});
184
185 if rate_a{act}(ia)>0
186 if node_p{act} ~= local
187 if node_p{act} == node_a{act} %self-loop, active and passive are the same
188 isf_p = isf_a;
189 if update_cond_p
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);
191 end
192 else % departure
193 isf_p = sn.nodeToStateful(node_p{act});
194 if update_cond_p
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);
196 end
197 end
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
201 else
202 prob_sync_p{act} = sync{act}.passive{1}.prob;
203 end
204 else
205 prob_sync_p{act} = 0;
206 end
207 end
208 if ~isempty(enabled_next_states{act}{isf_a})
209 if node_p{act} == local
210 prob_sync_p{act} = 1;
211 end
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};
219 end
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}));
224 end
225 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
226 enabled_sync(ctr) = act;
227 ctr = ctr + 1;
228 end
229 end
230 end
231 end
232 end
233 end
234 gctr_start = ctr;
235
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;
242 ctr = ctr + 1;
243
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};
250
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);
262 end
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);
270 end
271 end
272 end
273 end
274 end
275 end
276 else
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);
278 end
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
283
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.');
287 end
288 if selected_transition < gctr_start
289 % regular event pair
290 last_node_a = node_a{enabled_sync(selected_transition)};
291 last_node_p = node_p{enabled_sync(selected_transition)};
292 else % global event
293 last_node_a = NaN;
294 last_node_p = NaN;
295 end
296
297 %% Update paddings
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)
300 for ind=1:sn.nnodes
301 if sn.isstation(ind)
302 isf = sn.nodeToStateful(ind);
303 deltalen = length(cur_state{isf}) - statelen(isf);
304 if deltalen>0
305 statelen(isf) = length(cur_state{isf});
306 % here do padding
307 if ind==1
308 shift = 0;
309 else
310 shift = sum(statelen(1:isf-1));
311 end
312 pad = zeros(deltalen, size(tranState,2));
313 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
314 end
315 end
316 end
317
318 %% Simulate the time increment
319 state = cell2mat(cur_state');
320 dt = -(log(rand)/tot_rate);
321 cur_time = cur_time + dt;
322
323 %% Save simulation output data
324 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
325 tranSync(samples_collected,1) = enabled_sync(selected_transition);
326 for ind=1:sn.nnodes
327 if sn.isstation(ind)
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}(:);
332 end
333 end
334 SSq(:,samples_collected) = cell2mat(nir');
335
336 %% Update current state and sample counter
337 cur_state_1 = cur_state;
338 cur_state = enabled_next_states{enabled_sync(selected_transition)};
339
340 samples_collected = samples_collected + 1;
341
342 %% Print progress
343 print_progress(options,samples_collected);
344 end
345 % Print newline after progress counter
346 if options.verbose
347 line_printf('\n');
348 end
349catch ME
350 getReport(ME)
351end
352
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);
358
359%transient = min([floor(samples_collected/10),1000]); % remove first part of simulation (10% of the samples up to 1000 max)
360%transient = 0;
361%output = output((transient+1):end,:);
362tranState = tranState';
363
364
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))));
371end
372arvRates = zeros(size(u,1),sn.nstateful,R);
373depRates = zeros(size(u,1),sn.nstateful,R);
374
375pi = zeros(1,size(u,1));
376for s=1:size(u,1)
377 pi(s) = sum(tranState(uj==s,1));
378end
379SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
380
381for ind=1:sn.nnodes
382 if sn.isstateful(ind)
383 isf = sn.nodeToStateful(ind);
384 if sn.isstation(ind)
385 ist = sn.nodeToStation(ind);
386 %K = sn.phasessz(ist,:);
387 %Ks = sn.phaseshift(ist,:);
388 end
389 for s=1:size(u,1)
390 for r=1:R
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
393 end
394 end
395 end
396end
397pi = pi/sum(pi);
398%sn.nservers = init_nserver; % restore Inf at delay nodes
399end
400
401function print_progress(options,samples_collected)
402if options.verbose
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));
408 else
409 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
410 end
411 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
412 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
413 end
414end
415end
Definition mmt.m:92