LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
sn_fj_visits_spn.m
1function nodevisits = sn_fj_visits_spn(sn)
2% SN_FJ_VISITS_SPN Compute fork-join node visit ratios via auxiliary SPN models
3%
4% NODEVISITS = SN_FJ_VISITS_SPN(SN) builds, for each class that passes
5% through a fork-join pair, an auxiliary closed Stochastic Petri Net
6% capturing the fork/join synchronization semantics. The SPN is solved
7% with SolverCTMC and the throughput ratios give the per-node visit ratios.
8%
9% Population-preserving approach: the SPN uses population = B (number of
10% fork branches). The pre-fork station consumes B tokens and produces 1
11% per branch. The Join consumes 1 per branch and produces B. This ensures
12% token conservation and correct CTMC analysis.
13%
14% The function handles:
15% - Multiple fork-join pairs (including serial fork-joins)
16% - Multiple classes
17%
18% Returns:
19% nodevisits - cell(1, nchains), each entry is (nnodes x nclasses)
20% with visit ratios normalized to reference station = 1
21%
22% Copyright (c) 2012-2026, Imperial College London
23% All rights reserved.
24
25I = sn.nnodes;
26K = sn.nclasses;
27nchains = sn.nchains;
28inchain = sn.inchain;
29refstat = sn.refstat;
30
31nodevisits = cell(1, nchains);
32for c = 1:nchains
33 nodevisits{c} = zeros(I, K);
34end
35
36if ~any(sn.fj(:))
37 return
38end
39
40% For each chain, build and solve an SPN for each class in the chain
41for c = 1:nchains
42 for kidx = 1:length(inchain{c})
43 r = inchain{c}(kidx);
44
45 % Extract the single-class sub-routing from rtnodes
46 P_r = zeros(I, I);
47 for i = 1:I
48 for j = 1:I
49 P_r(i, j) = sn.rtnodes((i-1)*K + r, (j-1)*K + r);
50 end
51 end
52
53 % Find nodes visited by this class (reachable from reference station)
54 refnode = sn.stationToNode(refstat(r));
55 visited = false(1, I);
56 visited(refnode) = true;
57 changed = true;
58 while changed
59 changed = false;
60 for i = 1:I
61 if visited(i)
62 for j = 1:I
63 if P_r(i, j) > 0 && ~visited(j)
64 visited(j) = true;
65 changed = true;
66 end
67 end
68 end
69 end
70 end
71
72 % Skip if this class doesn't pass through any fork
73 has_fork = false;
74 for i = 1:I
75 if visited(i) && sn.nodetype(i) == NodeType.Fork
76 has_fork = true;
77 break;
78 end
79 end
80 if ~has_fork
81 for i = 1:I
82 if visited(i)
83 nodevisits{c}(i, r) = 1;
84 end
85 end
86 continue;
87 end
88
89 % Build auxiliary SPN model
90 nodevisits{c}(:, r) = build_and_solve_spn(sn, P_r, visited, r, refnode);
91 end
92
93 % Normalize by reference station
94 refnode_c = sn.stationToNode(refstat(inchain{c}(1)));
95 for kidx = 1:length(inchain{c})
96 r = inchain{c}(kidx);
97 normVal = nodevisits{c}(refnode_c, r);
98 if normVal > GlobalConstants.FineTol
99 nodevisits{c}(:, r) = nodevisits{c}(:, r) / normVal;
100 end
101 end
102end
103end
104
105function visits_r = build_and_solve_spn(sn, P_r, visited, r, refnode)
106% BUILD_AND_SOLVE_SPN Construct and solve auxiliary SPN for one class
107%
108% Uses a population-preserving approach:
109% - Population = total leaf branches (B) across all fork-join pairs
110% - Pre-fork station: requires B tokens, produces 1 per leaf branch
111% - Branch stations: normal 1-in-1-out service
112% - Join: requires 1 from each branch done, produces B in post-join dest
113% - Throughput ratios are uniform across all station Places => visits = 1
114
115I = sn.nnodes;
116visits_r = zeros(I, 1);
117
118visitedNodes = find(visited);
119nVisited = length(visitedNodes);
120if nVisited == 0
121 return;
122end
123
124% Classify visited nodes
125stationNodes = [];
126forkNodes = [];
127joinNodes = [];
128for idx = 1:nVisited
129 nd = visitedNodes(idx);
130 if sn.nodetype(nd) == NodeType.Fork
131 forkNodes(end+1) = nd; %#ok<AGROW>
132 elseif sn.nodetype(nd) == NodeType.Join
133 joinNodes(end+1) = nd; %#ok<AGROW>
134 elseif sn.isstation(nd) && sn.nodetype(nd) ~= NodeType.Source && sn.nodetype(nd) ~= NodeType.Sink
135 stationNodes(end+1) = nd; %#ok<AGROW>
136 end
137end
138
139% Compute leaf count for each Join node (bottom-up, handles arbitrary nesting)
140% join_leaves(jnd) = number of leaf tokens that flow through this Join
141join_leaves = containers.Map('KeyType', 'int32', 'ValueType', 'int32');
142for pass = 1:length(joinNodes) % iterate until stable
143 for jidx = 1:length(joinNodes)
144 jnd = joinNodes(jidx);
145 srcNodes_j = find(P_r(:, jnd) > 0 & visited');
146 lc = 0;
147 for si = 1:length(srcNodes_j)
148 srcNd = srcNodes_j(si);
149 if sn.nodetype(srcNd) == NodeType.Join && join_leaves.isKey(srcNd)
150 lc = lc + join_leaves(srcNd);
151 else
152 lc = lc + 1; % station or unresolved
153 end
154 end
155 join_leaves(jnd) = lc;
156 end
157end
158
159% Compute total leaf branches B (= population for conservation)
160% For each fork, resolve to leaf stations; B = max leaf count across outermost forks
161B = 0;
162for idx = 1:length(forkNodes)
163 fnd = forkNodes(idx);
164 % Check if this Fork is an outermost fork (not a descendant of another fork)
165 % An outermost fork has its upstream being a station, not another fork
166 srcNodes = find(P_r(:, fnd) > 0 & visited');
167 is_outermost = true;
168 for si = 1:length(srcNodes)
169 if sn.nodetype(srcNodes(si)) == NodeType.Join
170 is_outermost = false; % serial fork - Join feeds this Fork
171 break;
172 end
173 end
174 if is_outermost
175 leafs = resolve_fork_dests(sn, P_r, visited, fnd);
176 B = max(B, length(leafs));
177 end
178end
179if B == 0
180 B = 1;
181end
182
183% Build SPN model
184model = Network('fj_spn_aux');
185
186% Create Places for station nodes
187places = cell(1, I);
188for idx = 1:length(stationNodes)
189 nd = stationNodes(idx);
190 places{nd} = Place(model, sprintf('P_%s', sn.nodenames{nd}));
191end
192
193% Create "done" Places for stations that route to a Join
194pre_join = cell(1, I);
195for idx = 1:length(stationNodes)
196 nd = stationNodes(idx);
197 dstNodes = find(P_r(nd, :) > 0 & visited);
198 for di = 1:length(dstNodes)
199 if sn.nodetype(dstNodes(di)) == NodeType.Join
200 pre_join{nd} = Place(model, sprintf('P_%s_done', sn.nodenames{nd}));
201 break;
202 end
203 end
204end
205
206% For inner Joins that route to outer Joins (nested fork-join)
207for idx = 1:length(joinNodes)
208 jnd = joinNodes(idx);
209 dstNodes = find(P_r(jnd, :) > 0 & visited);
210 for di = 1:length(dstNodes)
211 if sn.nodetype(dstNodes(di)) == NodeType.Join
212 pre_join{jnd} = Place(model, sprintf('P_%s_done', sn.nodenames{jnd}));
213 break;
214 end
215 end
216end
217
218% Intermediate Places for Join -> Fork connections (serial fork-join)
219inter_jf = cell(1, I);
220for idx = 1:length(joinNodes)
221 jnd = joinNodes(idx);
222 dstNodes = find(P_r(jnd, :) > 0 & visited);
223 for di = 1:length(dstNodes)
224 if sn.nodetype(dstNodes(di)) == NodeType.Fork
225 inter_jf{jnd} = Place(model, sprintf('P_%s_to_%s', sn.nodenames{jnd}, sn.nodenames{dstNodes(di)}));
226 break;
227 end
228 end
229end
230
231% ClosedClass with B tokens at reference Place
232jobclass = ClosedClass(model, 'Token', B, places{refnode});
233
234transitions = {};
235transInfo = {};
236
237% Create timed service Transitions for each station
238for idx = 1:length(stationNodes)
239 nd = stationNodes(idx);
240 ist = sn.nodeToStation(nd);
241
242 % Service rate
243 is_timed = false;
244 rate = 0;
245 if ist > 0 && ~isempty(sn.rates) && ist <= size(sn.rates, 1) && r <= size(sn.rates, 2) && sn.rates(ist, r) > 0 && ~isinf(sn.rates(ist, r))
246 rate = sn.rates(ist, r);
247 is_timed = true;
248 end
249
250 % Determine effective output Places by resolving through Fork/Join routing
251 dstNodes = find(P_r(nd, :) > 0 & visited);
252 out_places = {};
253 enable_count = 1; % how many tokens to require/consume
254 for di = 1:length(dstNodes)
255 dstNd = dstNodes(di);
256 if sn.nodetype(dstNd) == NodeType.Fork
257 % Resolve Fork to leaf stations; require B tokens (population-preserving)
258 forkDests = resolve_fork_dests(sn, P_r, visited, dstNd);
259 enable_count = B; % require all B tokens
260 for fi = 1:length(forkDests)
261 if ~isempty(places{forkDests(fi)})
262 out_places{end+1} = places{forkDests(fi)}; %#ok<AGROW>
263 end
264 end
265 elseif sn.nodetype(dstNd) == NodeType.Join
266 if ~isempty(pre_join{nd})
267 out_places{end+1} = pre_join{nd}; %#ok<AGROW>
268 end
269 elseif sn.isstation(dstNd)
270 if ~isempty(places{dstNd})
271 out_places{end+1} = places{dstNd}; %#ok<AGROW>
272 end
273 end
274 end
275
276 if isempty(out_places), continue; end
277
278 tName = sprintf('T_svc_%s', sn.nodenames{nd});
279 T = Transition(model, tName);
280 mode = T.addMode('serve');
281
282 if is_timed
283 T.setDistribution(mode, Exp(rate));
284 else
285 T.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
286 T.setFiringWeights(mode, 1.0);
287 end
288
289 T.setEnablingConditions(mode, jobclass, places{nd}, enable_count);
290 T.setFiringOutcome(mode, jobclass, places{nd}, -enable_count);
291 for oi = 1:length(out_places)
292 T.setFiringOutcome(mode, jobclass, out_places{oi}, 1);
293 end
294
295 transitions{end+1} = T; %#ok<AGROW>
296 transInfo{end+1} = struct('type', 'service', 'nd', nd, ...
297 'inPlaces', {{places{nd}}}, 'outPlaces', {out_places}); %#ok<AGROW>
298end
299
300% Create Immediate Join Transitions
301for idx = 1:length(joinNodes)
302 jnd = joinNodes(idx);
303 srcNodes = find(P_r(:, jnd) > 0 & visited');
304 dstNodes = find(P_r(jnd, :) > 0 & visited);
305
306 % Collect input "done" Places and their source nodes
307 in_places = {};
308 in_src_nodes = [];
309 for si = 1:length(srcNodes)
310 srcNd = srcNodes(si);
311 if ~isempty(pre_join{srcNd})
312 in_places{end+1} = pre_join{srcNd}; %#ok<AGROW>
313 in_src_nodes(end+1) = srcNd; %#ok<AGROW>
314 end
315 end
316 if isempty(in_places), continue; end
317
318 % Compute enabling counts per input: 1 for stations, join_leaves for inner joins
319 en_counts = ones(1, length(in_places));
320 for ii = 1:length(in_src_nodes)
321 srcNd = in_src_nodes(ii);
322 if sn.nodetype(srcNd) == NodeType.Join && join_leaves.isKey(srcNd)
323 en_counts(ii) = join_leaves(srcNd);
324 end
325 end
326
327 % produce_count = join_leaves(jnd) = total leaf tokens through this Join
328 if join_leaves.isKey(jnd)
329 produce_count = join_leaves(jnd);
330 else
331 produce_count = sum(en_counts);
332 end
333
334 % Determine post-Join output Places
335 out_places = {};
336 for di = 1:length(dstNodes)
337 dstNd = dstNodes(di);
338 if sn.nodetype(dstNd) == NodeType.Fork
339 % Serial fork-join
340 if ~isempty(inter_jf{jnd})
341 out_places{end+1} = inter_jf{jnd}; %#ok<AGROW>
342 end
343 elseif sn.nodetype(dstNd) == NodeType.Join
344 % Nested: inner Join -> outer Join
345 if ~isempty(pre_join{jnd})
346 out_places{end+1} = pre_join{jnd}; %#ok<AGROW>
347 end
348 elseif sn.isstation(dstNd)
349 if ~isempty(places{dstNd})
350 out_places{end+1} = places{dstNd}; %#ok<AGROW>
351 end
352 end
353 end
354 if isempty(out_places), continue; end
355
356 tName = sprintf('T_join_%s', sn.nodenames{jnd});
357 T = Transition(model, tName);
358 mode = T.addMode('sync');
359 T.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
360 T.setFiringWeights(mode, 1.0);
361
362 for ii = 1:length(in_places)
363 T.setEnablingConditions(mode, jobclass, in_places{ii}, en_counts(ii));
364 T.setFiringOutcome(mode, jobclass, in_places{ii}, -en_counts(ii));
365 end
366 for oi = 1:length(out_places)
367 T.setFiringOutcome(mode, jobclass, out_places{oi}, produce_count);
368 end
369
370 transitions{end+1} = T; %#ok<AGROW>
371 transInfo{end+1} = struct('type', 'join', 'nd', jnd, ...
372 'inPlaces', {in_places}, 'outPlaces', {out_places}); %#ok<AGROW>
373end
374
375% Create Immediate Fork Transitions for Join -> Fork connections (serial FJ)
376for idx = 1:length(joinNodes)
377 jnd = joinNodes(idx);
378 if isempty(inter_jf{jnd}), continue; end
379
380 dstNodes = find(P_r(jnd, :) > 0 & visited);
381 for di = 1:length(dstNodes)
382 dstNd = dstNodes(di);
383 if sn.nodetype(dstNd) ~= NodeType.Fork, continue; end
384
385 forkDests = resolve_fork_dests(sn, P_r, visited, dstNd);
386 if isempty(forkDests), continue; end
387
388 tName = sprintf('T_fork_%s_%s', sn.nodenames{jnd}, sn.nodenames{dstNd});
389 T = Transition(model, tName);
390 mode = T.addMode('fork');
391 T.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
392 T.setFiringWeights(mode, 1.0);
393
394 T.setEnablingConditions(mode, jobclass, inter_jf{jnd}, B);
395 T.setFiringOutcome(mode, jobclass, inter_jf{jnd}, -B);
396 fork_out = {};
397 for fi = 1:length(forkDests)
398 if ~isempty(places{forkDests(fi)})
399 T.setFiringOutcome(mode, jobclass, places{forkDests(fi)}, 1);
400 fork_out{end+1} = places{forkDests(fi)}; %#ok<AGROW>
401 end
402 end
403
404 transitions{end+1} = T; %#ok<AGROW>
405 transInfo{end+1} = struct('type', 'fork', 'nd', jnd, ...
406 'inPlaces', {{inter_jf{jnd}}}, 'outPlaces', {fork_out}); %#ok<AGROW>
407 end
408end
409
410% Set up routing matrix
411R = model.initRoutingMatrix();
412for tidx = 1:length(transitions)
413 T = transitions{tidx};
414 info = transInfo{tidx};
415 for ii = 1:length(info.inPlaces)
416 R{1,1}(info.inPlaces{ii}, T) = 1;
417 end
418 for oi = 1:length(info.outPlaces)
419 R{1,1}(T, info.outPlaces{oi}) = 1;
420 end
421end
422model.link(R);
423
424% Set initial state: B tokens at reference, 0 everywhere else
425places{refnode}.setState(B);
426for i = 1:I
427 if ~isempty(places{i}) && i ~= refnode
428 places{i}.setState(0);
429 end
430 if ~isempty(pre_join{i})
431 pre_join{i}.setState(0);
432 end
433 if ~isempty(inter_jf{i})
434 inter_jf{i}.setState(0);
435 end
436end
437
438% Solve with CTMC
439try
440 solver = SolverCTMC(model);
441 avg = solver.getAvgTable();
442
443 % Extract throughput ratios as visit ratios
444 stationNames = avg.Station;
445 tputVals = avg.Tput;
446
447 for idx = 1:nVisited
448 nd = visitedNodes(idx);
449 if ~isempty(places{nd})
450 placeName = places{nd}.name;
451 rowIdx = find(strcmp(string(stationNames), placeName));
452 if ~isempty(rowIdx)
453 visits_r(nd) = tputVals(rowIdx(1));
454 end
455 end
456 end
457catch ME
458 line_warning(mfilename, sprintf('SPN CTMC solve failed for class %d: %s', r, ME.message));
459end
460end
461
462function stDests = resolve_fork_dests(sn, P_r, visited, forkNd)
463% RESOLVE_FORK_DESTS Recursively resolve Fork destinations to station nodes
464stDests = [];
465branchDests = find(P_r(forkNd, :) > 0 & visited);
466for bdi = 1:length(branchDests)
467 bd = branchDests(bdi);
468 if sn.nodetype(bd) == NodeType.Fork
469 stDests = [stDests, resolve_fork_dests(sn, P_r, visited, bd)]; %#ok<AGROW>
470 elseif sn.isstation(bd)
471 stDests = [stDests, bd]; %#ok<AGROW>
472 end
473end
474end
Definition mmt.m:124