LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getStruct.m
1function lsn = getStruct(self, regenerate)
2% LSN = GETSTRUCT(SELF, regenerate)
3%
4%
5% Copyright 2012-2026, Imperial College London
6if nargin<2
7 regenerate = false;
8end
9if ~isempty(self.lsn) && ~regenerate
10 lsn = self.lsn;
11 return
12end
13lsn = LayeredNetworkStruct();
14lsn.nidx = 0; % total number of hosts, tasks, entries, and activities, except the reference tasks
15lsn.hshift = 0;
16lsn.nhosts = length(self.hosts);
17lsn.ntasks = length(self.tasks);
18lsn.nentries = length(self.entries);
19lsn.nacts = length(self.activities);
20lsn.tshift = lsn.nhosts;
21lsn.eshift = lsn.nhosts + lsn.ntasks;
22lsn.ashift = lsn.nhosts + lsn.ntasks + lsn.nentries;
23lsn.cshift = lsn.nhosts + lsn.ntasks + lsn.nentries + lsn.nacts;
24
25%% analyze static properties
26lsn.nidx = lsn.nhosts + lsn.ntasks + lsn.nentries + lsn.nacts;
27idx = 1;
28lsn.tasksof = cell(lsn.nhosts,1);
29lsn.entriesof = cell(lsn.nhosts+lsn.ntasks,1);
30lsn.actsof = cell(lsn.nhosts+lsn.ntasks+lsn.nentries,1);
31lsn.callsof = cell(lsn.nacts,1);
32lsn.hostdem = {};
33lsn.hostdem_type = zeros(lsn.nidx, 1);
34lsn.hostdem_params = cell(lsn.nidx, 1);
35lsn.hostdem_mean = nan(lsn.nidx, 1);
36lsn.hostdem_scv = nan(lsn.nidx, 1);
37lsn.hostdem_proc = cell(lsn.nidx, 1);
38
39lsn.actthink = {};
40lsn.actthink_type = zeros(lsn.nidx, 1);
41lsn.actthink_params = cell(lsn.nidx, 1);
42lsn.actthink_mean = nan(lsn.nidx, 1);
43lsn.actthink_scv = nan(lsn.nidx, 1);
44lsn.actthink_proc = cell(lsn.nidx, 1);
45
46lsn.think = {};
47lsn.think_type = zeros(lsn.nidx, 1);
48lsn.think_params = cell(lsn.nidx, 1);
49lsn.think_mean = nan(lsn.nidx, 1);
50lsn.think_scv = nan(lsn.nidx, 1);
51lsn.think_proc = cell(lsn.nidx, 1);
52lsn.sched = [];
53lsn.names = {};
54lsn.hashnames = {};
55%lsn.shortnames = {};
56lsn.mult = zeros(lsn.nhosts+lsn.ntasks,1);
57lsn.maxmult = zeros(lsn.nhosts+lsn.ntasks,1);
58lsn.repl = zeros(lsn.nhosts+lsn.ntasks,1);
59lsn.type = zeros(lsn.nidx,1);
60lsn.graph = zeros(lsn.nidx,lsn.nidx);
61loop_back_edges = false(lsn.nidx,lsn.nidx);
62%lsn.replies = [];
63lsn.replygraph = false(lsn.nacts,lsn.nentries);
64lsn.actphase = ones(lsn.nacts,1); % Phase for each activity (default=1)
65
66lsn.nitems = zeros(lsn.nhosts+lsn.ntasks+lsn.nentries,1);
67lsn.itemcap = {};
68lsn.itemproc = {};
69lsn.itemproc_type = zeros(lsn.nidx, 1);
70lsn.itemproc_params = cell(lsn.nidx, 1);
71lsn.itemproc_mean = nan(lsn.nidx, 1);
72lsn.itemproc_scv = nan(lsn.nidx, 1);
73lsn.itemproc_proc = cell(lsn.nidx, 1);
74
75lsn.iscache = zeros(lsn.nhosts+lsn.ntasks,1);
76lsn.setuptime = cell(lsn.nhosts+lsn.ntasks,1);
77lsn.setuptime_type = zeros(lsn.nidx, 1);
78lsn.setuptime_params = cell(lsn.nidx, 1);
79lsn.setuptime_mean = nan(lsn.nidx, 1);
80lsn.setuptime_scv = nan(lsn.nidx, 1);
81lsn.setuptime_proc = cell(lsn.nidx, 1);
82
83lsn.delayofftime = {};
84lsn.delayofftime_type = zeros(lsn.nidx, 1);
85lsn.delayofftime_params = cell(lsn.nidx, 1);
86lsn.delayofftime_mean = nan(lsn.nidx, 1);
87lsn.delayofftime_scv = nan(lsn.nidx, 1);
88lsn.delayofftime_proc = cell(lsn.nidx, 1);
89
90lsn.isfunction = zeros(lsn.nhosts+lsn.ntasks,1);
91
92% Open arrival distributions
93lsn.arrival = {};
94lsn.arrival_type = zeros(lsn.nidx, 1);
95lsn.arrival_params = cell(lsn.nidx, 1);
96lsn.arrival_mean = nan(lsn.nidx, 1);
97lsn.arrival_scv = nan(lsn.nidx, 1);
98lsn.arrival_proc = cell(lsn.nidx, 1);
99
100lsn.parent = [];
101for p=1:lsn.nhosts % for every processor, scheduling, multiplicity, replication, names, type
102 lsn.sched(idx,1) = SchedStrategy.fromText(self.hosts{p}.scheduling);
103 lsn.mult(idx,1) = self.hosts{p}.multiplicity;
104 lsn.repl(idx,1) = self.hosts{p}.replication;
105 lsn.names{idx,1} = self.hosts{p}.name;
106 lsn.hashnames{idx,1} = ['P:',lsn.names{idx,1}];
107 %lsn.shortnames{idx,1} = ['P',num2str(p)];
108 lsn.type(idx,1) = LayeredNetworkElement.HOST; % processor
109 idx = idx + 1;
110end
111
112for t=1:lsn.ntasks
113 lsn.sched(idx,1) = SchedStrategy.fromText(self.tasks{t}.scheduling);
114 % hostdem for tasks is Immediate
115 immDist = Immediate.getInstance();
116 lsn.hostdem{idx,1} = immDist;
117 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(immDist);
118 % think time
119 thinkDist = self.tasks{t}.thinkTime;
120 lsn.think{idx,1} = thinkDist;
121 [lsn.think_type(idx), lsn.think_params{idx}, lsn.think_mean(idx), lsn.think_scv(idx), lsn.think_proc{idx}] = extractDistParams(thinkDist);
122 lsn.mult(idx,1) = self.tasks{t}.multiplicity;
123 lsn.repl(idx,1) = self.tasks{t}.replication;
124 lsn.names{idx,1} = self.tasks{t}.name;
125 switch lsn.sched(idx,1)
126 case SchedStrategy.REF
127 lsn.hashnames{idx,1} = ['R:',lsn.names{idx,1}];
128 %lsn.shortnames{idx,1} = ['R',num2str(idx-tshift)];
129 otherwise
130 lsn.hashnames{idx,1} = ['T:',lsn.names{idx,1}];
131 %lsn.shortnames{idx,1} = ['T',num2str(idx-tshift)];
132 end
133 switch class(self.tasks{t})
134 case 'CacheTask'
135 lsn.nitems(idx,1) = self.tasks{t}.items;
136 lsn.itemcap{idx,1} = self.tasks{t}.itemLevelCap;
137 lsn.replacestrat(idx,1) = self.tasks{t}.replacestrategy;
138 lsn.hashnames{idx,1} = ['C:',lsn.names{idx,1}];
139 case 'FunctionTask'
140 setupDist = self.tasks{t}.SetupTime;
141 lsn.setuptime{idx,1} = setupDist;
142 [lsn.setuptime_type(idx), lsn.setuptime_params{idx}, lsn.setuptime_mean(idx), lsn.setuptime_scv(idx), lsn.setuptime_proc{idx}] = extractDistParams(setupDist);
143 delayoffDist = self.tasks{t}.DelayOffTime;
144 lsn.delayofftime{idx,1} = delayoffDist;
145 [lsn.delayofftime_type(idx), lsn.delayofftime_params{idx}, lsn.delayofftime_mean(idx), lsn.delayofftime_scv(idx), lsn.delayofftime_proc{idx}] = extractDistParams(delayoffDist);
146 lsn.hashnames{idx,1} = ['F:',lsn.names{idx,1}];
147 %lsn.shortnames{idx,1} = ['C',num2str(idx-tshift)];
148 end
149 pidx = find(cellfun(@(x) strcmp(x.name, self.tasks{t}.parent.name), self.hosts));
150 lsn.parent(idx) = pidx;
151 lsn.graph(idx, pidx) = 1;
152 lsn.type(idx) = LayeredNetworkElement.TASK; % task
153 idx = idx + 1;
154end
155
156for p=1:lsn.nhosts % for every processor
157 pidx = p;
158 lsn.tasksof{pidx} = find(lsn.parent == pidx);
159end
160
161for e=1:lsn.nentries
162 lsn.names{idx,1} = self.entries{e}.name;
163
164 % Extract open arrival distribution if present
165 if ~isempty(self.entries{e}.arrival) && isa(self.entries{e}.arrival, 'Distribution')
166 arrDist = self.entries{e}.arrival;
167 lsn.arrival{idx,1} = arrDist;
168 [lsn.arrival_type(idx), lsn.arrival_params{idx}, lsn.arrival_mean(idx), lsn.arrival_scv(idx), lsn.arrival_proc{idx}] = extractDistParams(arrDist);
169 end
170
171 switch class(self.entries{e})
172 case 'Entry'
173 lsn.hashnames{idx,1} = ['E:',lsn.names{idx,1}];
174 for a=1:length(self.entries{e}.replyActivity)
175 ractname = self.entries{e}.replyActivity{a};
176 ractidx = find(cellfun(@(x) strcmp(x.name, ractname), self.activities));
177 lsn.replygraph(ractidx,e)=true;
178 end
179 %lsn.shortnames{idx,1} = ['E',num2str(idx-eshift)];
180 case 'ItemEntry'
181 lsn.hashnames{idx,1} = ['I:',lsn.names{idx,1}];
182 %lsn.shortnames{idx,1} = ['I',num2str(idx-eshift)];
183 lsn.nitems(idx,1) = self.entries{e}.cardinality;
184 itemDist = self.entries{e}.popularity;
185 lsn.itemproc{idx,1} = itemDist;
186 [lsn.itemproc_type(idx), lsn.itemproc_params{idx}, lsn.itemproc_mean(idx), lsn.itemproc_scv(idx), lsn.itemproc_proc{idx}] = extractDistParams(itemDist);
187 end
188 % hostdem for entries is Immediate
189 immDist = Immediate.getInstance();
190 lsn.hostdem{idx,1} = immDist;
191 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(immDist);
192 tidx = lsn.nhosts + find(cellfun(@(x) strcmp(x.name, self.entries{e}.parent.name), self.tasks));
193 lsn.parent(idx) = tidx;
194 lsn.graph(tidx,idx) = 1;
195 lsn.entriesof{tidx}(end+1) = idx;
196 lsn.type(idx) = LayeredNetworkElement.ENTRY; % entries
197 idx = idx + 1;
198end
199
200for a=1:lsn.nacts
201 lsn.names{idx,1} = self.activities{a}.name;
202 lsn.hashnames{idx,1} = ['A:',lsn.names{idx,1}];
203 %lsn.shortnames{idx,1} = ['A',num2str(idx - lsn.ashift)];
204 % hostDemand for activities
205 hostdemDist = self.activities{a}.hostDemand;
206 lsn.hostdem{idx,1} = hostdemDist;
207 [lsn.hostdem_type(idx), lsn.hostdem_params{idx}, lsn.hostdem_mean(idx), lsn.hostdem_scv(idx), lsn.hostdem_proc{idx}] = extractDistParams(hostdemDist);
208 % thinkTime for activities
209 actthinkDist = self.activities{a}.thinkTime;
210 lsn.actthink{idx,1} = actthinkDist;
211 [lsn.actthink_type(idx), lsn.actthink_params{idx}, lsn.actthink_mean(idx), lsn.actthink_scv(idx), lsn.actthink_proc{idx}] = extractDistParams(actthinkDist);
212 tidx = lsn.nhosts + find(cellfun(@(x) strcmp(x.name, self.activities{a}.parent.name), self.tasks));
213 lsn.parent(idx) = tidx;
214 lsn.actsof{tidx}(end+1) = idx;
215 lsn.type(idx) = LayeredNetworkElement.ACTIVITY; % activities
216 lsn.actphase(a) = self.activities{a}.phase; % Store activity phase
217 idx = idx + 1;
218end
219
220nidx = idx - 1; % number of indices
221lsn.graph(nidx,nidx) = 0;
222
223tasks = self.tasks;
224%% now analyze calls
225cidx = 0;
226lsn.calltype = sparse([],lsn.nidx,1);
227lsn.iscaller = sparse(lsn.nidx,lsn.nidx);
228lsn.issynccaller = sparse(lsn.nidx,lsn.nidx);
229lsn.isasynccaller = sparse(lsn.nidx,lsn.nidx);
230lsn.callpair = [];
231lsn.callproc = {};
232lsn.callproc_type = [];
233lsn.callproc_params = {};
234lsn.callproc_mean = [];
235lsn.callproc_scv = [];
236lsn.callproc_proc = {};
237lsn.callnames = {};
238lsn.callhashnames = {};
239%lsn.callshortnames = {};
240lsn.taskgraph = sparse(lsn.tshift+lsn.ntasks, lsn.tshift+lsn.ntasks);
241lsn.actpretype = sparse(lsn.nidx,1);
242lsn.actposttype = sparse(lsn.nidx,1);
243
244% Track boundToEntry mappings to validate uniqueness
245boundEntries = {};
246boundActivities = {};
247
248for t = 1:lsn.ntasks
249 tidx = lsn.tshift+t;
250 for a=1:length(self.tasks{t}.activities)
251 aidx = findstring(lsn.hashnames, ['A:',tasks{t}.activities(a).name]);
252 lsn.callsof{aidx} = [];
253 boundToEntry = tasks{t}.activities(a).boundToEntry;
254 %for b=1:length(boundToEntry)
255 eidx = findstring(lsn.hashnames, ['E:',boundToEntry]);
256 if eidx<0
257 eidx = findstring(lsn.hashnames, ['I:',boundToEntry]);
258 end
259 if eidx>0
260 lsn.graph(eidx, aidx) = 1;
261
262 % Check if this entry is already bound to another activity
263 activityName = tasks{t}.activities(a).name;
264 existingIdx = find(strcmp(boundEntries, boundToEntry), 1);
265 if ~isempty(existingIdx)
266 line_error(mfilename, sprintf('Multiple activities (%s, %s) are bound to the same entry: %s', ...
267 boundActivities{existingIdx}, activityName, boundToEntry));
268 else
269 boundEntries{end+1} = boundToEntry;
270 boundActivities{end+1} = activityName;
271 end
272 end
273 %end
274
275 for s=1:length(tasks{t}.activities(a).syncCallDests)
276 target_eidx = findstring(lsn.hashnames, ['E:',tasks{t}.activities(a).syncCallDests{s}]);
277 if target_eidx < 0
278 target_eidx = findstring(lsn.hashnames, ['I:',tasks{t}.activities(a).syncCallDests{s}]);
279 end
280 target_tidx = lsn.parent(target_eidx);
281 cidx = cidx + 1;
282 lsn.calltype(cidx,1) = CallType.SYNC;
283 lsn.callpair(cidx,1:2) = [aidx,target_eidx];
284 if tidx == target_tidx
285 line_error(mfilename, 'An entry on a task cannot call another entry on the same task.');
286 end
287 lsn.callnames{cidx,1} = [lsn.names{aidx},'=>',lsn.names{target_eidx}];
288 lsn.callhashnames{cidx,1} = [lsn.hashnames{aidx},'=>',lsn.hashnames{target_eidx}];
289 %lsn.callshortnames{cidx,1} = [lsn.shortnames{aidx},'=>',lsn.shortnames{target_eidx}];
290 callDist = Geometric(1/tasks{t}.activities(a).syncCallMeans(s)); % synch
291 lsn.callproc{cidx,1} = callDist;
292 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
293 lsn.callsof{aidx}(end+1) = cidx;
294 lsn.iscaller(tidx, target_tidx) = true;
295 lsn.iscaller(aidx, target_tidx) = true;
296 lsn.iscaller(tidx, target_eidx) = true;
297 lsn.iscaller(aidx, target_eidx) = true;
298 lsn.issynccaller(tidx, target_tidx) = true;
299 lsn.issynccaller(aidx, target_tidx) = true;
300 lsn.issynccaller(tidx, target_eidx) = true;
301 lsn.issynccaller(aidx, target_eidx) = true;
302 lsn.taskgraph(tidx, target_tidx) = 1;
303 lsn.graph(aidx, target_eidx) = 1;
304 end
305
306 for s=1:length(tasks{t}.activities(a).asyncCallDests)
307 target_entry_name = tasks{t}.activities(a).asyncCallDests{s};
308 target_eidx = findstring(lsn.hashnames,['E:',target_entry_name]);
309 if target_eidx < 0
310 target_eidx = findstring(lsn.hashnames, ['I:',target_entry_name]);
311 end
312 % Validate that the target entry exists
313 if target_eidx <= 0
314 line_error(mfilename, sprintf('Activity "%s" has an async call to non-existent entry "%s".', tasks{t}.activities(a).name, target_entry_name));
315 end
316 target_tidx = lsn.parent(target_eidx);
317 % Check for self-referential async calls (task calling itself)
318 if tidx == target_tidx
319 line_error(mfilename, sprintf('Activity "%s" in task "%s" has an async call to an entry on the same task. Async self-calls are not supported.', tasks{t}.activities(a).name, tasks{t}.name));
320 end
321 cidx = cidx + 1;
322 lsn.callpair(cidx,1:2) = [aidx,target_eidx];
323 lsn.calltype(cidx,1) = CallType.ASYNC; % async
324 lsn.callnames{cidx,1} = [lsn.names{aidx},'->',lsn.names{target_eidx}];
325 lsn.callhashnames{cidx,1} = [lsn.hashnames{aidx},'->',lsn.hashnames{target_eidx}];
326 %lsn.callshortnames{cidx,1} = [lsn.shortnames{aidx},'->',lsn.shortnames{target_eidx}];
327 callDist = Geometric(1/tasks{t}.activities(a).asyncCallMeans(s)); % asynch
328 lsn.callproc{cidx,1} = callDist;
329 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
330 lsn.callsof{aidx}(end+1) = cidx;
331 lsn.iscaller(aidx, target_tidx) = true;
332 lsn.iscaller(aidx, target_eidx) = true;
333 lsn.iscaller(tidx, target_tidx) = true;
334 lsn.iscaller(tidx, target_eidx) = true;
335 lsn.isasynccaller(tidx, target_tidx) = true;
336 lsn.isasynccaller(tidx, target_eidx) = true;
337 lsn.isasynccaller(aidx, target_tidx) = true;
338 lsn.isasynccaller(aidx, target_eidx) = true;
339 lsn.taskgraph(tidx, target_tidx) = 1;
340 lsn.graph(aidx, target_eidx) = 1;
341 end
342 end
343
344 for ap=1:length(tasks{t}.precedences)
345 pretype = tasks{t}.precedences(ap).preType;
346 posttype = tasks{t}.precedences(ap).postType;
347 preacts = tasks{t}.precedences(ap).preActs;
348 postacts = tasks{t}.precedences(ap).postActs;
349
350 % Validate PRE_AND activities exist before processing
351 if pretype == ActivityPrecedenceType.PRE_AND
352 if isempty(preacts)
353 line_error(mfilename, sprintf('PRE_AND precedence in task "%s" has no pre activities.', tasks{t}.name));
354 end
355 for prea = 1:length(preacts)
356 preaidx = findstring(lsn.hashnames, ['A:',preacts{prea}]);
357 if preaidx <= 0
358 line_error(mfilename, sprintf('PRE_AND precedence references non-existent activity "%s" in task "%s".', preacts{prea}, tasks{t}.name));
359 end
360 if preaidx > 0 && lsn.parent(preaidx) ~= tidx
361 line_error(mfilename, sprintf('PRE_AND precedence in task "%s" references activity "%s" from a different task.', tasks{t}.name, preacts{prea}));
362 end
363 end
364 end
365
366 % Validate POST_AND activities exist before processing
367 if posttype == ActivityPrecedenceType.POST_AND
368 if isempty(postacts)
369 line_error(mfilename, sprintf('POST_AND precedence in task "%s" has no post activities.', tasks{t}.name));
370 end
371 for posta = 1:length(postacts)
372 postaidx = findstring(lsn.hashnames, ['A:',postacts{posta}]);
373 if postaidx <= 0
374 line_error(mfilename, sprintf('POST_AND precedence references non-existent activity "%s" in task "%s".', postacts{posta}, tasks{t}.name));
375 end
376 if postaidx > 0 && lsn.parent(postaidx) ~= tidx
377 line_error(mfilename, sprintf('POST_AND precedence in task "%s" references activity "%s" from a different task.', tasks{t}.name, postacts{posta}));
378 end
379 end
380 end
381
382 for prea = 1:length(preacts)
383 preaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).preActs{prea}]);
384 switch pretype
385 case ActivityPrecedenceType.PRE_AND
386 quorum = tasks{t}.precedences(ap).preParams;
387 if isempty(quorum)
388 preParam = 1.0;
389 else
390 preParam = quorum / length(preacts);
391 end
392 otherwise
393 preParam = 1.0;
394 end
395 switch posttype
396 case ActivityPrecedenceType.POST_OR
397 for posta = 1:length(postacts)
398 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
399 probs = tasks{t}.precedences(ap).postParams;
400 postParam = probs(posta);
401 lsn.graph(preaidx, postaidx) = preParam * postParam;
402 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
403 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
404 end
405 case ActivityPrecedenceType.POST_AND
406 for posta = 1:length(postacts)
407 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
408 lsn.graph(preaidx, postaidx) = 1;
409 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
410 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
411 end
412 case ActivityPrecedenceType.POST_LOOP
413 counts = tasks{t}.precedences(ap).postParams;
414 % add the end activity
415 enda = length(postacts);
416 loopentryaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).preActs{1}]);
417 loopstartaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{1}]);
418 loopendaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{enda}]);
419
420 if counts < 1
421 % When expected iterations < 1, we may skip loop entirely
422 % E[iterations] = counts means P(enter loop) = counts
423 lsn.graph(loopentryaidx, loopstartaidx) = counts;
424 lsn.graph(loopentryaidx, loopendaidx) = 1.0 - counts;
425 % Process activities inside the loop as serial chain
426 curaidx = loopstartaidx;
427 for posta = 2:(length(postacts)-1)
428 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
429 lsn.graph(curaidx, postaidx) = 1.0;
430 lsn.actposttype(postaidx) = sparse((tasks{t}.precedences(ap).postType));
431 curaidx = postaidx;
432 end
433 % After loop body, always exit to end (no looping back)
434 lsn.graph(curaidx, loopendaidx) = 1.0;
435 lsn.actposttype(loopstartaidx) = sparse((tasks{t}.precedences(ap).postType));
436 else
437 % When expected iterations >= 1, always enter loop
438 % E[iterations] = 1/(1-p) = counts => p = 1 - 1/counts
439 curaidx = loopentryaidx;
440 for posta = 1:(length(postacts)-1) % last one is 'end' of loop activity
441 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
442 lsn.graph(curaidx, postaidx) = 1.0;
443 lsn.actposttype(postaidx) = sparse((tasks{t}.precedences(ap).postType));
444 curaidx = postaidx;
445 end
446 loop_back_edges(curaidx, loopstartaidx) = true;
447 lsn.graph(curaidx, loopstartaidx) = 1.0 - 1.0 / counts;
448 lsn.graph(curaidx, loopendaidx) = 1.0 / counts;
449 end
450 lsn.actposttype(loopendaidx) = sparse((tasks{t}.precedences(ap).postType));
451 otherwise
452 for posta = 1:length(postacts)
453 postaidx = findstring(lsn.hashnames, ['A:',tasks{t}.precedences(ap).postActs{posta}]);
454 postParam = 1.0;
455 lsn.graph(preaidx, postaidx) = preParam * postParam;
456 lsn.actpretype(preaidx) = sparse(tasks{t}.precedences(ap).preType);
457 lsn.actposttype(postaidx) = sparse(tasks{t}.precedences(ap).postType);
458 end
459 end
460 end
461 end
462end
463
464%% Process forwarding calls from entries
465for e = 1:length(self.entries)
466 entry = self.entries{e};
467 eidx = findstring(lsn.hashnames, ['E:', entry.name]);
468 if eidx <= 0
469 eidx = findstring(lsn.hashnames, ['I:', entry.name]);
470 end
471 if eidx <= 0
472 continue;
473 end
474 source_tidx = lsn.parent(eidx);
475
476 for fw = 1:length(entry.forwardingDests)
477 target_entry_name = entry.forwardingDests{fw};
478 target_eidx = findstring(lsn.hashnames, ['E:', target_entry_name]);
479 if target_eidx <= 0
480 target_eidx = findstring(lsn.hashnames, ['I:', target_entry_name]);
481 end
482 if target_eidx <= 0
483 line_error(mfilename, sprintf('Entry "%s" forwards to non-existent entry "%s".', entry.name, target_entry_name));
484 end
485 target_tidx = lsn.parent(target_eidx);
486
487 % Validate: cannot forward to same task
488 if source_tidx == target_tidx
489 line_error(mfilename, sprintf('Entry "%s" cannot forward to entry "%s" on the same task.', entry.name, target_entry_name));
490 end
491
492 cidx = cidx + 1;
493 lsn.calltype(cidx, 1) = CallType.FWD;
494 lsn.callpair(cidx, 1:2) = [eidx, target_eidx];
495 lsn.callnames{cidx, 1} = [lsn.names{eidx}, '~>', lsn.names{target_eidx}];
496 lsn.callhashnames{cidx, 1} = [lsn.hashnames{eidx}, '~>', lsn.hashnames{target_eidx}];
497
498 % Forwarding probability (stored as mean calls)
499 fwdProb = entry.forwardingProbs(fw);
500 callDist = Geometric(1.0 / fwdProb);
501 lsn.callproc{cidx, 1} = callDist;
502 [lsn.callproc_type(cidx), lsn.callproc_params{cidx}, lsn.callproc_mean(cidx), lsn.callproc_scv(cidx), lsn.callproc_proc{cidx}] = extractDistParams(callDist);
503
504 % Update task graph to reflect forwarding relationship
505 lsn.taskgraph(source_tidx, target_tidx) = 1;
506 lsn.graph(eidx, target_eidx) = 1;
507
508 % NOTE: We do NOT set issynccaller for forwarding calls because:
509 % - Forwarding is NOT a sync call - the forwarder does NOT wait for the target
510 % - The forwarder sends the request to the target and the target replies
511 % directly to the original caller (not back to the forwarder)
512 % - The forwarding target's workload comes from the forwarding probability,
513 % not from a separate closed class in the queueing network decomposition
514 %
515 % Setting issynccaller here was causing incorrect throughput calculations
516 % because it created separate closed classes for forwarding targets.
517 end
518end
519
520% Check for entries without boundTo activities
521unbound = find(all(~lsn.graph(lsn.eshift+1 : lsn.eshift+lsn.nentries, ...
522 lsn.ashift+1 : lsn.ashift+lsn.nacts), 2)); %#ok<EFIND>
523if ~isempty(unbound)
524 line_error(mfilename, 'An entry does not have any boundTo activity.');
525end
526
527%lsn.replies = false(1,lsn.nacts);
528%lsn.replygraph = 0*lsn.graph;
529% Snapshot which entries have explicit replies (from repliesTo calls)
530% before adding implicit ones. This way OrFork branches all get marked.
531hasExplicitReply = any(lsn.replygraph, 1);
532for t = 1:lsn.ntasks
533 tidx = lsn.tshift+t;
534 for aidx = lsn.actsof{tidx}
535 postaidxs = find(lsn.graph(aidx, :));
536 isreply = true;
537 % if no successor is an action of tidx
538 for postaidx = postaidxs
539 if any(lsn.actsof{tidx} == postaidx)
540 isreply = false;
541 end
542 end
543 if isreply
544 % this is a leaf node, search backward for the parent entry,
545 % which is assumed to be unique
546 %lsn.replies(aidx-lsn.nacts) = true;
547 parentidx = aidx;
548 while lsn.type(parentidx) ~= LayeredNetworkElement.ENTRY
549 ancestors = find(lsn.graph(:,parentidx));
550 parentidx = at(ancestors,1); % only choose first ancestor
551 end
552 if lsn.type(parentidx) == LayeredNetworkElement.ENTRY
553 eidx = parentidx - lsn.eshift;
554 % Only add implicit reply if no explicit reply exists for this entry
555 % This supports Phase-2 activities (activities after an explicit reply)
556 if ~hasExplicitReply(eidx)
557 lsn.replygraph(aidx-lsn.ashift, eidx) = true;
558 end
559 end
560 end
561 end
562end
563lsn.ncalls = size(lsn.calltype,1);
564
565% correct multiplicity for infinite server stations
566for tidx = find(lsn.sched == SchedStrategy.INF)
567 if lsn.type(tidx) == LayeredNetworkElement.TASK
568 callers = find(lsn.taskgraph(:, tidx));
569 callers_inf = strcmp(lsn.mult(callers), SchedStrategy.INF);
570 if any(callers_inf)
571 % if a caller is also inf, then we would need to recursively
572 % determine the maximum multiplicity, we instead use a
573 % heuristic value
574 lsn.mult(tidx) = sum(lsn.mult(~callers_inf)) + sum(callers_inf)*max(lsn.mult);
575 else
576 lsn.mult(tidx) = sum(lsn.mult(callers));
577 end
578 end
579end
580
581lsn.refset = zeros(lsn.nidx,1);
582[conncomps, roots]=graph_connected_components(lsn.taskgraph(lsn.nhosts+1:end, lsn.nhosts+1:end));
583lsn.conntasks = conncomps;
584for r=1:length(roots)
585 lsn.conntasks(find(lsn.conntasks == r)) = lsn.tshift+roots(r);
586end
587
588lsn.isref = lsn.sched == SchedStrategy.REF;
589lsn.iscache(1:(lsn.tshift+lsn.ntasks)) = lsn.nitems(1:(lsn.tshift+lsn.ntasks))>0;
590lsn.isfunction(1:(lsn.tshift+lsn.ntasks)) = ~cellfun(@isempty, lsn.setuptime(1:(lsn.tshift+lsn.ntasks)));
591
592% the dag differs from the graph:
593% - dag swaps the direction of entry-task edges
594% - dag removes loop edges
595dag = lsn.graph;
596n = size(dag, 1);
597% Reverse edges from TASK to ENTRY if not a reference
598for i = 1:n
599 if lsn.type(i) == LayeredNetworkElement.TASK && ~lsn.isref(i)
600 for j = 1:n
601 if lsn.type(j) == LayeredNetworkElement.ENTRY && dag(i,j)
602 dag(i,j) = 0;
603 dag(j,i) = 1;
604 end
605 end
606 end
607end
608
609% Compute entry-to-activity reachability within the same task
610for eoff = 1:lsn.nentries
611 eidx = lsn.eshift + eoff; % global entry index
612 tidx = lsn.parent(eidx); % parent task index
613 visited = false(1,nidx); % global visit mask
614 stack = eidx;
615 visited(eidx) = true;
616 while ~isempty(stack)
617 v = stack(end);
618 stack(end) = [];
619 nbrs = find(lsn.graph(v,:));
620 nbrs = nbrs(~visited(nbrs));
621 visited(nbrs) = true;
622 stack = [stack nbrs]; %#ok<AGROW>
623 end
624 acts = find(visited & ...
625 lsn.type' == LayeredNetworkElement.ACTIVITY & ...
626 lsn.parent == tidx);
627 lsn.actsof{lsn.eshift+eoff} = acts;
628end
629
630%% check for errors
631dag(loop_back_edges(:)) = 0;
632lsn.dag = dag;
633% Compute bounds on multiplicies for host processors and non-ref tasks
634if is_dag(dag)
635 lsn.maxmult = lsn_max_multiplicity(lsn);
636 lsn.maxmult = lsn.maxmult(1:(lsn.tshift+lsn.ntasks));
637else
638 line_error(mfilename, 'A cycle exists in an activity graph.');
639end
640% Check for non-terminal reply activities
641% An activity that replies to an entry should not have Phase 1 successor activities
642% Phase 2 successors are allowed (post-reply processing)
643for a = 1:lsn.nacts
644 if any(lsn.replygraph(a, :)) % activity 'a' replies to some entry
645 aidx = lsn.ashift + a; % global activity index
646 successors = find(lsn.graph(aidx, :));
647 for succ = successors
648 if succ > lsn.eshift + lsn.nentries % successor is an activity
649 succ_act_idx = succ - lsn.ashift; % convert to activity array index
650 if lsn.actphase(succ_act_idx) == 1
651 line_error(mfilename, 'Unsupported replyTo in non-terminal activity.');
652 end
653 end
654 end
655 end
656end
657
658% Check calls
659if ~isempty(lsn.callpair)
660 target_eidxs = unique(lsn.callpair(:,2));
661 for eidx=target_eidxs(:)'
662 call_types_to_eidx = lsn.calltype(find(lsn.callpair(:,2) == eidx),1);
663 if ~all(call_types_to_eidx == call_types_to_eidx(1))
664 line_error(mfilename, 'An entry is called both synchronously and asynchronously.');
665 end
666 end
667end
668
669self.lsn = lsn;
670end
671
672function [dtype, params, mean_val, scv_val, proc] = extractDistParams(dist)
673% EXTRACTDISTPARAMS Extract primitive parameters from a Distribution object
674%
675% Returns:
676% dtype - ProcessType enum value
677% params - Vector/cell of primitive parameters
678% mean_val - Precomputed mean (NaN if unavailable)
679% scv_val - Precomputed SCV (NaN if unavailable)
680% proc - Process representation {D0, D1} or [] for non-Markovian
681
682if isempty(dist)
683 dtype = ProcessType.DISABLED;
684 params = [];
685 mean_val = NaN;
686 scv_val = NaN;
687 proc = {};
688 return
689end
690
691% Get mean and SCV
692try
693 mean_val = dist.getMean();
694catch
695 mean_val = NaN;
696end
697
698try
699 scv_val = dist.getSCV();
700catch
701 scv_val = NaN;
702end
703
704% Get process representation if available
705try
706 proc = dist.getRepres();
707 if isempty(proc)
708 proc = {};
709 end
710catch
711 proc = {};
712end
713
714% Extract parameters based on distribution type
715distClass = class(dist);
716switch distClass
717 case 'Disabled'
718 dtype = ProcessType.DISABLED;
719 params = [];
720
721 case 'Immediate'
722 dtype = ProcessType.IMMEDIATE;
723 params = [];
724 mean_val = 0;
725 scv_val = 0;
726
727 case 'Exp'
728 dtype = ProcessType.EXP;
729 params = dist.getParam(1).paramValue; % lambda
730
731 case 'Erlang'
732 dtype = ProcessType.ERLANG;
733 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [k; mu]
734
735 case 'HyperExp'
736 dtype = ProcessType.HYPEREXP;
737 % p1, lambda1, lambda2
738 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue; dist.getParam(3).paramValue];
739
740 case 'Coxian'
741 dtype = ProcessType.COXIAN;
742 mu = dist.getParam(1).paramValue;
743 phi = dist.getParam(2).paramValue;
744 params = [length(mu); mu(:); phi(:)];
745
746 case 'Cox2'
747 dtype = ProcessType.COX2;
748 mu1 = dist.getParam(1).paramValue;
749 mu2 = dist.getParam(2).paramValue;
750 phi = dist.getParam(3).paramValue;
751 params = [mu1; mu2; phi];
752
753 case 'APH'
754 dtype = ProcessType.APH;
755 alpha = dist.getParam(1).paramValue;
756 T = dist.getParam(2).paramValue;
757 n = length(alpha);
758 params = [n; alpha(:); T(:)];
759
760 case 'PH'
761 dtype = ProcessType.PH;
762 alpha = dist.getParam(1).paramValue;
763 T = dist.getParam(2).paramValue;
764 n = length(alpha);
765 params = [n; alpha(:); T(:)];
766
767 case 'Det'
768 dtype = ProcessType.DET;
769 params = dist.getParam(1).paramValue;
770
771 case 'Uniform'
772 dtype = ProcessType.UNIFORM;
773 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [a; b]
774
775 case 'Gamma'
776 dtype = ProcessType.GAMMA;
777 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
778
779 case 'Pareto'
780 dtype = ProcessType.PARETO;
781 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
782
783 case 'Weibull'
784 dtype = ProcessType.WEIBULL;
785 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [shape; scale]
786
787 case 'Lognormal'
788 dtype = ProcessType.LOGNORMAL;
789 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [mu; sigma]
790
791 case 'MAP'
792 dtype = ProcessType.MAP;
793 D0 = dist.getParam(1).paramValue;
794 D1 = dist.getParam(2).paramValue;
795 n = size(D0,1);
796 params = [n; D0(:); D1(:)];
797
798 case 'MMPP2'
799 dtype = ProcessType.MMPP2;
800 lambda0 = dist.getParam(1).paramValue;
801 lambda1 = dist.getParam(2).paramValue;
802 sigma0 = dist.getParam(3).paramValue;
803 sigma1 = dist.getParam(4).paramValue;
804 params = [lambda0; lambda1; sigma0; sigma1];
805
806 case 'Geometric'
807 dtype = ProcessType.GEOMETRIC;
808 params = dist.getParam(1).paramValue; % p (success probability)
809
810 case 'Poisson'
811 dtype = ProcessType.POISSON;
812 params = dist.getParam(1).paramValue; % lambda
813
814 case 'Binomial'
815 dtype = ProcessType.BINOMIAL;
816 params = [dist.getParam(1).paramValue; dist.getParam(2).paramValue]; % [n; p]
817
818 case 'Bernoulli'
819 dtype = ProcessType.BERNOULLI;
820 params = dist.getParam(1).paramValue; % p
821
822 case {'Replayer', 'Trace'}
823 dtype = ProcessType.REPLAYER;
824 params = []; % Trace data not stored as primitive
825
826 case 'DiscreteSampler'
827 dtype = ProcessType.GEOMETRIC; % Approximation
828 params = [];
829
830 otherwise
831 % Fallback for unhandled types
832 dtype = ProcessType.DISABLED;
833 params = [];
834 proc = {};
835end
836end