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<DLABINDEX>
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 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);
88 end
89 if isinf(sn.nservers(ist))
90 sn.nservers(ist) = capacity_sum;
91 end
92 sn.cap(ist,:) = capacity_sum;
93 sn.classcap(ist,:) = capacityc(ind,:);
94 end
95end
96
97%%
98if any(isinf(Np))
99 Np(isinf(Np)) = 0;
100end
101
102init_state_hashed = ones(1,nstateful); % pick the first state in init_state{i}
103
104%%
105arvRatesSamples = zeros(options.samples,nstateful,R);
106depRatesSamples = zeros(options.samples,nstateful,R);
107A = length(sync);
108G = length(gsync);
109samples_collected = 1;
110nir = {};
111% fill stateCell with initial states
112cur_state = cell(nstateful,1); % cell array with current stateful node states
113for ind=1:sn.nnodes
114 if sn.isstateful(ind)
115 isf = sn.nodeToStateful(ind);
116 cur_state{isf} = init_state{isf}(init_state_hashed(isf),:);
117 if sn.isstation(ind)
118 ist = sn.nodeToStation(ind);
119 [~,nir{ist}] = State.toMarginal(sn, ind, init_state{isf}(init_state_hashed(isf),:));
120 nir{ist} = nir{ist}(:);
121 end
122 end
123end
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');
136local = sn.nnodes+1;
137last_node_a = 0; % active in the last occurred synchronization
138last_node_p = 0; % passive in the last occurred synchronization
139for act=1:A
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;
146 outprob_a{act} = [];
147 outprob_p{act} = [];
148end
149enabled_next_states = cell(1,A);
150
151%% Start main simulation loop
152isSimulation = true; % allow state vector to grow, e.g. for FCFS buffers
153samples_collected = 1;
154cur_time = 0;
155use_inline = true; % true = stable version, false = dev version
156
157try
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
161 if use_inline
162 enabled_sync = []; % row is action label, col1=rate, col2=new state
163 enabled_rates = [];
164 ctr = 1;
165 A = length(sync);
166 G = length(gsync);
167 for act=1:A
168 isf_a = sn.nodeToStateful(node_a{act});
169
170 enabled_next_states{act} = cur_state;
171 update_cond_a = true;
172 if update_cond_a
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);
174 end
175
176 if isempty(enabled_next_states{act}{isf_a}) || isempty(rate_a{act})
177 continue
178 end
179
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
184 % never selected
185 rate_a{act}(ia) = 1e-38; % ~ zero in 32-bit precision
186 end
187
188 if enabled_next_states{act}{isf_a}(ia,:) == -1 % hash not found
189 continue
190 end
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});
192
193 if rate_a{act}(ia)>0
194 if node_p{act} ~= local
195 if node_p{act} == node_a{act} %self-loop, active and passive are the same
196 isf_p = isf_a;
197 if update_cond_p
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);
199 end
200 else % departure
201 isf_p = sn.nodeToStateful(node_p{act});
202 if update_cond_p
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);
204 end
205 end
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
209 else
210 prob_sync_p{act} = sync{act}.passive{1}.prob;
211 end
212 else
213 prob_sync_p{act} = 0;
214 end
215 end
216 if ~isempty(enabled_next_states{act}{isf_a})
217 if node_p{act} == local
218 prob_sync_p{act} = 1;
219 end
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};
227 end
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}));
232 end
233 enabled_rates(ctr) = rate_a{act}(ia) * prob_sync_p{act};
234 enabled_sync(ctr) = act;
235 ctr = ctr + 1;
236 end
237 end
238 end
239 end
240 end
241 end
242 gctr_start = ctr;
243
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;
250 ctr = ctr + 1;
251
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};
258
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);
270 end
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);
278 end
279 end
280 end
281 end
282 end
283 end
284 else
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);
286 end
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
291
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.');
295 end
296 if selected_transition < gctr_start
297 % regular event pair
298 last_node_a = node_a{enabled_sync(selected_transition)};
299 last_node_p = node_p{enabled_sync(selected_transition)};
300 else % global event
301 last_node_a = NaN;
302 last_node_p = NaN;
303 end
304
305 %% Update paddings
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)
308 for ind=1:sn.nnodes
309 if sn.isstation(ind)
310 isf = sn.nodeToStateful(ind);
311 deltalen = length(cur_state{isf}) - statelen(isf);
312 if deltalen>0
313 statelen(isf) = length(cur_state{isf});
314 % here do padding
315 if ind==1
316 shift = 0;
317 else
318 shift = sum(statelen(1:isf-1));
319 end
320 pad = zeros(deltalen, size(tranState,2));
321 tranState = [tranState(1:(shift+1), :); pad ; tranState((shift+1+deltalen):end, :)];
322 end
323 end
324 end
325
326 %% Simulate the time increment
327 state = cell2mat(cur_state');
328 dt = -(log(rand)/tot_rate);
329 cur_time = cur_time + dt;
330
331 %% Save simulation output data
332 tranState(1:(1+length(state)),samples_collected) = [dt, state]';
333 tranSync(samples_collected,1) = enabled_sync(selected_transition);
334 for ind=1:sn.nnodes
335 if sn.isstation(ind)
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}(:);
340 end
341 end
342 SSq(:,samples_collected) = cell2mat(nir');
343
344 %% Update current state and sample counter
345 cur_state_1 = cur_state;
346 cur_state = enabled_next_states{enabled_sync(selected_transition)};
347
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
360 np_a = [];
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};
365 end
366 if ~isempty(np_a) && isfield(np_a, 'withMemory') && np_a.withMemory
367 isf_a = sn.nodeToStateful(aNode);
368 Rcl = sn.nclasses;
369 if ~isempty(cur_state{isf_a}) && length(cur_state{isf_a}) >= Rcl
370 cur_state{isf_a}(end - Rcl + aClass) = node_p{actSync};
371 end
372 end
373 end
374 end
375 end
376
377 samples_collected = samples_collected + 1;
378
379 %% Print progress
380 print_progress(options,samples_collected);
381 end
382 % Print newline after progress counter
383 if options.verbose
384 line_printf('\n');
385 end
386catch ME
387 getReport(ME)
388end
389
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);
395
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).
399warmupfrac = 0.0;
400if isfield(options, 'config') && isfield(options.config, 'warmupfrac')
401 warmupfrac = max(0.0, min(0.99, options.config.warmupfrac));
402end
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, :);
409 SSq = SSq(:, keep);
410 if exist('arvRatesSamples', 'var') && ~isempty(arvRatesSamples) ...
411 && size(arvRatesSamples, 1) >= samples_collected
412 arvRatesSamples = arvRatesSamples(keep, :, :);
413 end
414 if exist('depRatesSamples', 'var') && ~isempty(depRatesSamples) ...
415 && size(depRatesSamples, 1) >= samples_collected
416 depRatesSamples = depRatesSamples(keep, :, :);
417 end
418 samples_collected = numel(keep);
419 end
420end
421
422tranState = tranState';
423
424
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))));
431end
432arvRates = zeros(size(u,1),sn.nstateful,R);
433depRates = zeros(size(u,1),sn.nstateful,R);
434
435pi = zeros(1,size(u,1));
436for s=1:size(u,1)
437 pi(s) = sum(tranState(uj==s,1));
438end
439SSq = SSq(:,ui)'; % we restrict to unique states in the simulation
440
441for ind=1:sn.nnodes
442 if sn.isstateful(ind)
443 isf = sn.nodeToStateful(ind);
444 if sn.isstation(ind)
445 ist = sn.nodeToStation(ind);
446 %K = sn.phasessz(ist,:);
447 %Ks = sn.phaseshift(ist,:);
448 end
449 for s=1:size(u,1)
450 for r=1:R
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
453 end
454 end
455 end
456end
457pi = pi/sum(pi);
458%sn.nservers = init_nserver; % restore Inf at delay nodes
459end
460
461function print_progress(options,samples_collected)
462if options.verbose
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));
468 else
469 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
470 end
471 elseif mod(samples_collected,1e2)==0 || options.verbose == 2
472 line_printf(sprintf('\b\b\b\b\b\b%6d',samples_collected));
473 end
474end
475end
Definition mmt.m:124