LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
LQN2QN.m
1function model = LQN2QN(lqn)
2% LQN2QN Convert a LayeredNetwork (LQN) to a Network (QN) using REPLY signals
3%
4% model = LQN2QN(lqn) flattens a LayeredNetwork into a single queueing
5% network in which synchronous call blocking is represented by REPLY
6% signals.
7%
8% Construction
9% - One station per host processor (scheduling and multiplicity taken from
10% the processor). Tasks sharing a processor share the station, as in the
11% LQN semantics where the processor is the contended resource.
12% - One Delay per reference task, holding its think time.
13% - One closed class per step of the expanded activity graph. A step is an
14% activity, or one call stage of an activity that issues synchronous
15% calls. Steps of the reference task chain carry population 0 except the
16% think class, which carries the reference task multiplicity.
17% - A synchronous call site blocks its caller: the step class has a REPLY
18% signal bound to it (sn.syncreply), the token proceeds to the callee,
19% and the callee's replying activity class-switches to that signal, which
20% returns to the caller station and unblocks it.
21% - Call multiplicity mean m is unrolled into floor(m) mandatory call
22% stages plus, if m is not integer, one further stage entered with
23% probability m-floor(m). This preserves both the mean number of calls
24% and the blocking of each individual call.
25% - OR-branch and loop probabilities are read from the activity graph
26% weights lsn.graph(a,b). Multiple entries per task and branching call
27% trees are expanded, each call site receiving its own copy of the callee
28% subgraph so that per-call-path response times remain distinguishable.
29%
30% - AND precedences become Fork and Join nodes: a POST_AND successor set is
31% entered through a Fork and one Router per branch, since a Fork cannot
32% switch class per output link, and the PRE_AND branch tails switch back
33% to the class that entered the Fork, which is the class the Join matches
34% its siblings on. A branch tail that issues a synchronous call is given a
35% merge step, so that it reaches the Join in an ordinary class rather than
36% as a REPLY signal, which carries no forked-task identity.
37%
38% - A CacheTask becomes a Cache node. The activity bound to an ItemEntry is
39% the read step and sits on that node, carrying the item popularity and
40% cardinality of the entry; its two CacheAccess successors become the hit
41% and the miss class, and the class switch is performed by the Cache node
42% itself, so the routes leaving it are written in the successor class.
43%
44% - An asynchronous call is lowered to a non-blocking visit: the caller does
45% not hold its server for the duration of the call, but it is serialised
46% behind it, since a closed network has no means of creating the second
47% token that a truly concurrent send would require.
48%
49% - Entry forwarding splits the reply exits of the forwarding entry: with
50% the forwarding probability the request is handed to the target entry,
51% which replies to the original caller, so the forwarder is released while
52% the caller stays blocked.
53%
54% - The multiplicity of a non-reference task is its thread pool: at most
55% that many requests may be inside the task at once, where inside spans
56% the task's own steps and those of its nested callees, since a thread is
57% held for the whole of a synchronous call. It is enforced by a finite
58% capacity region with one linear admission constraint per task, and the
59% calls of such a task do not hold the caller's server, since the
60% processor is released while a thread waits for a reply.
61%
62% - An entry with an open arrival process receives requests from a Source:
63% they traverse the entry's subgraph in open classes and leave through a
64% Sink where the entry would reply. Calls on an open chain do not hold
65% the caller's server, since a REPLY signal is a closed class that cannot
66% be woven into an open chain; the thread pools of the traversed tasks
67% are still enforced by the finite capacity region.
68%
69% - Phase-2 activities, the successors of a replying activity, run after the
70% reply: the replying step's exit routes back to the caller, and each of
71% its service completions spawns the continuation at the host station
72% (sn.classspawn). The spawned token walks the phase-2 subgraph holding
73% only the task's own thread and is destroyed at the chain end, through a
74% NEGATIVE signal that always misses on a closed chain, or through the
75% Sink on an open one. Where the replying activity's exit is not a plain
76% station departure (it ends on a call site, an AND precedence or a cache
77% read), phase 2 falls back to running before the reply, with a warning.
78%
79% Not yet represented: delayed-hit retrieval on the cache miss path, and the
80% thread pool of a task with an internal AND-fork. Each is reported through
81% line_warning.
82%
83% Example:
84% lqn = LayeredNetwork('MyLQN');
85% % ... define LQN model ...
86% model = LQN2QN(lqn);
87% SolverLDES(model).getAvgTable()
88%
89% Copyright (c) 2012-2026, Imperial College London
90% All rights reserved.
91
92lsn = lqn.getStruct();
93model = Network([lqn.getName(), '-QN']);
94
95refTaskIndices = find(lsn.isref);
96
97%% Entries with an open arrival process
98% Requests of such an entry arrive from the environment through a Source,
99% traverse the entry's expanded subgraph in open classes, and leave through
100% a Sink where the entry would reply. Calls issued along an open chain do
101% not hold the caller's server, so that no REPLY signal, a closed class,
102% has to be woven into an open chain; the thread pools of the tasks
103% traversed are still enforced by the finite capacity region.
104openEntries = [];
105if isfield(lsn, 'arrival') && ~isempty(lsn.arrival)
106 for e_ = lsn.eshift+1:lsn.eshift+lsn.nentries
107 if e_ <= length(lsn.arrival) && isa(lsn.arrival{e_}, 'Distribution') && ...
108 isfinite(lsn.arrival{e_}.getMean()) && lsn.arrival{e_}.getMean() > GlobalConstants.FineTol
109 openEntries(end+1) = e_; %#ok<AGROW>
110 end
111 end
112end
113
114if isempty(refTaskIndices) && isempty(openEntries)
115 line_error(mfilename, 'LQN must have at least one reference task or open arrival.');
116end
117
118MAXCALLSTAGES = 20; % guard against unrolling a huge call multiplicity
119
120%% Unsupported features, reported once each
121warnUnsupported(lsn);
122
123%% Tasks whose multiplicity is a thread pool
124% Such a task holds one thread per request from entry to reply, also across
125% its nested synchronous calls. Its calls do not hold the caller's server,
126% and a finite capacity region caps the jobs across its step classes at the
127% task multiplicity instead. A task with an internal AND-fork is excluded:
128% its forked siblings would double-count the thread that spawned them.
129fcrTask = false(lsn.nhosts + lsn.ntasks, 1);
130for t_ = 1:lsn.ntasks
131 tidx_ = lsn.tshift + t_;
132 if lsn.isref(tidx_) || (tidx_ <= length(lsn.iscache) && lsn.iscache(tidx_))
133 continue;
134 end
135 if ~isfinite(lsn.mult(tidx_)) || lsn.sched(tidx_) == SchedStrategy.INF
136 continue;
137 end
138 if taskHasAndFork(tidx_)
139 line_warning(mfilename, sprintf(['Multiplicity of task %s is not enforced: ' ...
140 'an AND-fork inside a task cannot be capped by a finite capacity ' ...
141 'region, whose job count would double-count the forked siblings.'], ...
142 lsn.names{tidx_}));
143 continue;
144 end
145 fcrTask(tidx_) = true;
146end
147
148% A task called, transitively, from inside an AND-fork branch is excluded
149% too: the fork-join transformation retags branch flows into auxiliary
150% classes, which would silently bypass the task's admission row. Such a
151% task keeps the server-hold blocking semantics instead.
152branchActs = [];
153if isfield(lsn, 'actposttype') && ~isempty(lsn.actposttype)
154 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
155 if a_ > length(lsn.actposttype) || full(lsn.actposttype(a_)) ~= ActivityPrecedenceType.POST_AND
156 continue;
157 end
158 frontier_ = a_;
159 while ~isempty(frontier_)
160 cur_ = frontier_(1); frontier_(1) = [];
161 if any(branchActs == cur_)
162 continue;
163 end
164 branchActs(end+1) = cur_; %#ok<AGROW>
165 if isfield(lsn, 'actpretype') && ~isempty(lsn.actpretype) && ...
166 cur_ <= length(lsn.actpretype) && ...
167 full(lsn.actpretype(cur_)) == ActivityPrecedenceType.PRE_AND
168 continue; % branch tail: do not traverse past the join
169 end
170 succ_ = find(lsn.graph(cur_, :));
171 succ_ = succ_(succ_ > lsn.ashift & lsn.parent(succ_).' == lsn.parent(cur_));
172 frontier_ = [frontier_, succ_]; %#ok<AGROW>
173 end
174 end
175end
176if ~isempty(branchActs) && any(fcrTask)
177 front_ = [];
178 for a_ = branchActs
179 if a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
180 for c_ = lsn.callsof{a_}
181 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
182 end
183 end
184 end
185 shadow_ = false(size(fcrTask));
186 while ~isempty(front_)
187 t_ = front_(1); front_(1) = [];
188 if shadow_(t_)
189 continue;
190 end
191 shadow_(t_) = true;
192 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
193 if lsn.parent(a_) == t_ && a_ <= length(lsn.callsof) && ~isempty(lsn.callsof{a_})
194 for c_ = lsn.callsof{a_}
195 front_(end+1) = lsn.parent(lsn.callpair(c_, 2)); %#ok<AGROW>
196 end
197 end
198 end
199 end
200 for t_ = find(shadow_(:).' & fcrTask(:).')
201 fcrTask(t_) = false;
202 line_warning(mfilename, sprintf(['Multiplicity of task %s is not enforced: ' ...
203 'it is called from inside an AND-fork branch, whose flows the ' ...
204 'fork-join transformation retags outside the admission constraint.'], ...
205 lsn.names{t_}));
206 end
207end
208
209%% Stations: one per host processor
210hostStation = cell(lsn.nhosts, 1);
211hostIsDelay = false(lsn.nhosts, 1);
212for h = 1:lsn.nhosts
213 nservers = lsn.mult(h);
214 sched = lsn.sched(h);
215 if isinf(nservers) || sched == SchedStrategy.INF
216 hostStation{h} = Delay(model, lsn.names{h});
217 hostIsDelay(h) = true;
218 else
219 q = Queue(model, lsn.names{h}, sched);
220 q.setNumberOfServers(nservers);
221 hostStation{h} = q;
222 end
223end
224
225%% Think delays, one per reference task
226thinkNode = containers.Map('KeyType', 'double', 'ValueType', 'any');
227for rt = 1:length(refTaskIndices)
228 refTidx = refTaskIndices(rt);
229 thinkNode(refTidx) = Delay(model, [lsn.names{refTidx}, '_Think']);
230end
231
232%% Pass 1: expand the activity graph into a step graph
233% Steps carry no LINE objects yet, so that all classes can be created in
234% one block before the routing matrix is initialised.
235stepAidx = []; % activity index of the step
236stepHost = []; % host processor index of the step station
237stepSvc = {}; % service distribution at the step station, [] if none
238stepName = {}; % class name
239stepBlocks = []; % true if the step blocks on a synchronous call
240stepIsThink = []; % true for a think step (reference task)
241stepRefTask = []; % reference task the step belongs to
242stepNode = {}; % Fork/Join/Router node the step sits on, [] for stations
243stepClassOwner = []; % step whose class this step travels in (itself, normally)
244stepTasks = {}; % thread-pool tasks holding a thread while at this step
245
246% flow(i,:) = [fromStep, toStep, prob, fromIsSignal, inTargetClass]. A step
247% is left either through its own class or, when it is a blocking call site,
248% through the reply signal that unblocks it. A Cache node switches class on
249% its own, so the routes leaving it are written in the class of the target
250% step, which is what inTargetClass marks.
251flow = zeros(0, 5);
252% reply(i,:) = [calleeExitStep, callerStep, calleeExitIsSignal, prob]: class
253% switch into the caller's reply signal, destined for the caller station.
254% prob is below one only where the callee entry forwards part of its replies.
255reply = zeros(0, 4);
256% spawnPairs(i,:) = [triggerStep, targetStep]: each service completion of the
257% trigger class spawns a job of the target class at the same station
258% (sn.classspawn), which is how a phase-2 continuation survives the reply.
259spawnPairs = zeros(0, 2);
260% ph2Exits(i,:) = [step, isSignal, prob, refTidx]: end of a phase-2 chain.
261% The spawned token is destroyed there: on a closed chain it switches into a
262% NEGATIVE signal aimed at a station nothing visits, whose miss annihilates
263% it; on an open chain it leaves through the Sink.
264ph2Exits = zeros(0, 4);
265
266entryStack = []; % guards against recursive call cycles
267threadStack = []; % tasks holding a thread during the current expansion; it
268 % tracks entryStack except across forwarding, which hands
269 % the request on and releases the forwarder's thread
270
271% Cache nodes, one per CacheTask, and the read/hit/miss wiring to apply once
272% the classes exist.
273cacheNodeOf = containers.Map('KeyType', 'double', 'ValueType', 'any');
274cacheWiring = struct('node', {}, 'readStep', {}, 'hitStep', {}, 'missStep', {}, ...
275 'itemproc', {}, 'nitems', {});
276
277for rt = 1:length(refTaskIndices)
278 refTidx = refTaskIndices(rt);
279 thinkStep = addStep([], [], [], [lsn.names{refTidx}, '_Think'], false, true, refTidx);
280
281 entries = lsn.entriesof{refTidx};
282 for eidx = entries
283 [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx);
284 if isempty(firstStep)
285 continue;
286 end
287 addRoute([thinkStep, 0], firstStep, 1.0);
288 % A reference task has no caller: its replies and its dead ends
289 % both close the cycle at the think delay.
290 exits = [replyExits; terminals];
291 for s = 1:size(exits, 1)
292 addRoute(exits(s, 1:2), thinkStep, exits(s, 3));
293 end
294 end
295end
296
297%% Pass 1b: open arrival chains
298% openWiring rows: {eidx, firstStep, exits}, wired to Source and Sink in
299% pass 4 once the classes exist. The reference task index 0 marks a step of
300% an open chain.
301openWiring = struct('eidx', {}, 'firstStep', {}, 'exits', {});
302srcNode = []; snkNode = [];
303if ~isempty(openEntries)
304 srcNode = Source(model, 'Source');
305 snkNode = Sink(model, 'Sink');
306end
307for eidx = openEntries
308 [firstStep, replyExits, terminals] = expandEntry(eidx, 0);
309 if isempty(firstStep)
310 line_warning(mfilename, sprintf('Open arrival entry %s has no bound activity; ignored.', ...
311 lsn.names{eidx}));
312 continue;
313 end
314 openWiring(end+1) = struct('eidx', eidx, 'firstStep', firstStep, ...
315 'exits', [replyExits; terminals]); %#ok<AGROW>
316end
317
318%% Pass 2: create classes and reply signals
319nsteps = length(stepAidx);
320stepClass = cell(nsteps, 1);
321stepSignal = cell(nsteps, 1);
322
323for i = 1:nsteps
324 refTidx = stepRefTask(i);
325 if stepClassOwner(i) ~= i
326 % Fork, Join and Router steps carry the job through unchanged: they
327 % travel in the class of the step that entered the fork, which is
328 % also the class a Join matches its siblings on.
329 continue;
330 end
331 if refTidx == 0
332 % A step of an open arrival chain travels in an open class.
333 stepClass{i} = OpenClass(model, stepName{i});
334 elseif stepIsThink(i)
335 population = lsn.mult(refTidx);
336 stepClass{i} = ClosedClass(model, stepName{i}, population, thinkNode(refTidx));
337 else
338 stepClass{i} = ClosedClass(model, stepName{i}, 0, thinkNode(refTidx));
339 end
340end
341for i = 1:nsteps
342 stepClass{i} = stepClass{stepClassOwner(i)};
343end
344
345for i = 1:nsteps
346 if stepBlocks(i)
347 sig = Signal(model, [stepName{i}, '_Reply'], SignalType.REPLY);
348 sig.forJobClass(stepClass{i});
349 stepSignal{i} = sig;
350 end
351end
352
353% Signal.m installs RAND routing at every node; clear it so that link()
354% only honours the routes set below.
355for i = 1:nsteps
356 if ~isempty(stepSignal{i})
357 for n = 1:length(model.nodes)
358 if ~isa(model.nodes{n}, 'Sink')
359 model.nodes{n}.setRouting(stepSignal{i}, RoutingStrategy.DISABLED);
360 end
361 end
362 end
363end
364
365% Spawn bindings for phase-2 continuations: each completion of the trigger
366% class injects a job of the target class at the same station.
367for sp = 1:size(spawnPairs, 1)
368 stepClass{spawnPairs(sp, 1)}.setSpawnClass(stepClass{spawnPairs(sp, 2)});
369end
370
371% Destructor for spawned phase-2 tokens on closed chains: the end of a
372% phase-2 chain switches into a NEGATIVE signal aimed at a station that
373% nothing else visits, so the signal always misses and is annihilated. One
374% destructor per reference task, so that each stays in the chain of the
375% phase-2 classes it collects, whose reference station it shares.
376ph2DumpNode = [];
377ph2DestructorOf = containers.Map('KeyType', 'double', 'ValueType', 'any');
378if ~isempty(ph2Exits) && any(ph2Exits(:, 4) > 0)
379 ph2DumpNode = Queue(model, 'Ph2Sink', SchedStrategy.FCFS);
380 for rft = unique(ph2Exits(ph2Exits(:, 4) > 0, 4).')
381 sig = ClosedSignal(model, sprintf('Ph2End_%s', lsn.names{rft}), ...
382 SignalType.NEGATIVE, thinkNode(rft));
383 for n = 1:length(model.nodes)
384 if ~isa(model.nodes{n}, 'Sink')
385 model.nodes{n}.setRouting(sig, RoutingStrategy.DISABLED);
386 end
387 end
388 ph2DumpNode.setService(sig, Immediate());
389 ph2DestructorOf(rft) = sig;
390 end
391end
392
393%% Pass 3: service times
394for i = 1:nsteps
395 if ~isempty(stepNode{i})
396 % Fork/Join/Router nodes hold no service, but a Router-hosted merge
397 % step owns a class, which must have a service pair somewhere: it is
398 % declared Immediate at the reference think delay, as for signals.
399 if stepClassOwner(i) == i && isa(stepNode{i}, 'Router')
400 if stepRefTask(i) == 0
401 % Open chain: no think delay exists, declare the pair at the
402 % caller's host station instead, which the class never visits.
403 hostStation{stepHost(i)}.setService(stepClass{i}, Immediate());
404 else
405 tn_ = thinkNode(stepRefTask(i));
406 tn_.setService(stepClass{i}, Immediate());
407 end
408 end
409 continue;
410 end
411 if stepIsThink(i)
412 refTidx = stepRefTask(i);
413 thinkDist = lsn.think{refTidx};
414 tnode = thinkNode(refTidx);
415 if isempty(thinkDist) || isa(thinkDist, 'Immediate') || thinkDist.getMean() < GlobalConstants.FineTol
416 tnode.setService(stepClass{i}, Immediate());
417 else
418 tnode.setService(stepClass{i}, thinkDist);
419 end
420 else
421 station = hostStation{stepHost(i)};
422 if isempty(stepSvc{i})
423 station.setService(stepClass{i}, Immediate());
424 else
425 station.setService(stepClass{i}, stepSvc{i});
426 end
427 end
428end
429
430% A reply signal is consumed at the station of the caller it unblocks and
431% passed on without further service. Service is declared at every station
432% because a signal that finds no pending reply falls back to its reference
433% station, which must not be a disabled pair.
434for i = 1:nsteps
435 if ~isempty(stepSignal{i})
436 for h = 1:lsn.nhosts
437 hostStation{h}.setService(stepSignal{i}, Immediate());
438 end
439 tkeys = cell2mat(thinkNode.keys);
440 for kk = tkeys
441 tn = thinkNode(kk);
442 tn.setService(stepSignal{i}, Immediate());
443 end
444 end
445end
446
447%% Cache read/hit/miss wiring, now that the classes exist
448for w = 1:length(cacheWiring)
449 cw = cacheWiring(w);
450 cw.node.setReadItemEntry(stepClass{cw.readStep}, cw.itemproc, cw.nitems);
451 cw.node.setHitClass(stepClass{cw.readStep}, stepClass{cw.hitStep});
452 cw.node.setMissClass(stepClass{cw.readStep}, stepClass{cw.missStep});
453end
454
455%% Pass 4: routing
456P = model.initRoutingMatrix();
457
458for e = 1:size(flow, 1)
459 i = flow(e, 1); j = flow(e, 2); p = flow(e, 3);
460 if flow(e, 5)
461 P{stepClass{j}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
462 elseif flow(e, 4)
463 P{stepSignal{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
464 else
465 P{stepClass{i}, stepClass{j}}(stationOf(i), stationOf(j)) = p;
466 end
467end
468
469for e = 1:size(reply, 1)
470 i = reply(e, 1); owner = reply(e, 2); viaSignal = reply(e, 3); p = reply(e, 4);
471 if viaSignal
472 % A nested call returns through its own reply signal, which
473 % class-switches into the reply signal of the outer call site.
474 P{stepSignal{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
475 else
476 P{stepClass{i}, stepSignal{owner}}(stationOf(i), stationOf(owner)) = p;
477 end
478end
479
480%% Phase-2 chain ends: destroy the spawned token
481for e = 1:size(ph2Exits, 1)
482 i = ph2Exits(e, 1); viaSig = ph2Exits(e, 2); p = ph2Exits(e, 3); rft = ph2Exits(e, 4);
483 if rft == 0
484 % Open chain: the spawned token leaves through the Sink.
485 ecls = stepClass{i};
486 P{ecls, ecls}(stationOf(i), snkNode) = p;
487 elseif viaSig
488 P{stepSignal{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
489 else
490 P{stepClass{i}, ph2DestructorOf(rft)}(stationOf(i), ph2DumpNode) = p;
491 end
492end
493
494%% Open arrival wiring: Source into the first step, exits into the Sink
495for w = 1:length(openWiring)
496 ow = openWiring(w);
497 firstCls = stepClass{ow.firstStep};
498 srcNode.setArrival(firstCls, lsn.arrival{ow.eidx});
499 P{firstCls, firstCls}(srcNode, stationOf(ow.firstStep)) = 1.0;
500 for s = 1:size(ow.exits, 1)
501 % Open chains carry no signals, so every exit is an ordinary class.
502 ecls = stepClass{ow.exits(s, 1)};
503 P{ecls, ecls}(stationOf(ow.exits(s, 1)), snkNode) = ow.exits(s, 3);
504 end
505end
506
507model.link(P);
508
509%% Thread pools: one finite capacity region, one linear constraint per task
510% The region spans every station visited by a thread-pool task's step
511% classes. Each task contributes one admission row A(t,:)*x <= mult(t),
512% where x counts in-region jobs per class, so a job entering the task, or
513% one of its nested callees, waits outside until a thread is free. Class
514% coefficients are shared across rows where tasks nest, which keeps
515% intra-region class switches admissible: the sums are left unchanged.
516fcrList = find(fcrTask(:).');
517if ~isempty(fcrList)
518 Amat = zeros(length(fcrList), length(model.classes));
519 bvec = zeros(length(fcrList), 1);
520 regionNodes = {};
521 for tsel = 1:length(fcrList)
522 for stp = 1:nsteps
523 if isempty(stepTasks{stp}) || ~any(stepTasks{stp} == fcrList(tsel))
524 continue;
525 end
526 Amat(tsel, stepClass{stp}.index) = 1;
527 nd = stationOf(stp);
528 if isa(nd, 'Station') && ~any(cellfun(@(x) x == nd, regionNodes))
529 regionNodes{end+1} = nd; %#ok<AGROW>
530 end
531 end
532 bvec(tsel) = lsn.mult(fcrList(tsel));
533 end
534 if any(Amat(:)) && ~isempty(regionNodes)
535 fcr = model.addRegion(regionNodes);
536 fcr.setConstraint(Amat, bvec);
537 end
538end
539
540%% ---------------------------------------------------------------- helpers
541
542 function node = stationOf(i)
543 if ~isempty(stepNode{i})
544 node = stepNode{i};
545 elseif stepIsThink(i)
546 node = thinkNode(stepRefTask(i));
547 else
548 node = hostStation{stepHost(i)};
549 end
550 end
551
552 function id = addStep(aidx, hidx, svc, name, blocks, isthink, refTidx)
553 stepAidx(end+1) = ifempty(aidx, 0); %#ok<AGROW>
554 stepHost(end+1) = ifempty(hidx, 0); %#ok<AGROW>
555 stepSvc{end+1} = svc; %#ok<AGROW>
556 stepName{end+1} = name; %#ok<AGROW>
557 stepBlocks(end+1) = blocks; %#ok<AGROW>
558 stepIsThink(end+1) = isthink; %#ok<AGROW>
559 stepRefTask(end+1) = refTidx; %#ok<AGROW>
560 stepNode{end+1} = []; %#ok<AGROW>
561 id = length(stepAidx);
562 stepClassOwner(end+1) = id; %#ok<AGROW>
563 % A thread of every thread-pool task on the stack is held while a
564 % job is at this step, since a synchronous caller does not release
565 % its thread until the reply.
566 if isempty(threadStack)
567 stepTasks{end+1} = []; %#ok<AGROW>
568 else
569 stepTasks{end+1} = unique(threadStack(fcrTask(threadStack))); %#ok<AGROW>
570 end
571 end
572
573 function id = addAuxStep(nodeObj, ownerStep, name, refTidx)
574 % A step on a Fork, Join or Router node: no station, no service, and
575 % no class of its own.
576 id = addStep([], [], [], name, false, false, refTidx);
577 stepNode{id} = nodeObj;
578 stepClassOwner(id) = stepClassOwner(ownerStep);
579 end
580
581 function [firstStep, replyExits, terminals] = expandEntry(eidx, refTidx)
582 % Expands the activity subgraph bound to an entry, in the call
583 % context given by the current entry stack.
584 firstStep = [];
585 replyExits = zeros(0, 3);
586 terminals = zeros(0, 3);
587
588 if any(entryStack == eidx)
589 line_warning(mfilename, sprintf('Recursive call cycle at entry %s truncated.', lsn.names{eidx}));
590 return;
591 end
592 entryStack(end+1) = eidx;
593 threadStack(end+1) = lsn.parent(eidx);
594 restore = onCleanup(@() popEntry());
595
596 if eidx > length(lsn.actsof) || isempty(lsn.actsof{eidx})
597 return;
598 end
599
600 % The bound activity is the activity successor of the entry.
601 succ = find(lsn.graph(eidx, :));
602 boundActs = succ(arrayfun(@(a) a <= length(lsn.type) && lsn.type(a) == LayeredNetworkElement.ACTIVITY, succ));
603 boundActs = intersect(boundActs, lsn.actsof{eidx}, 'stable');
604 if isempty(boundActs)
605 return;
606 end
607
608 [firstStep, replyExits, terminals] = expandActivities(boundActs(1), eidx, refTidx);
609
610 % Forwarding: with probability p the entry hands the request on to
611 % another entry instead of replying, and that entry replies directly
612 % to the original caller. The forwarder is released at that point,
613 % while the caller stays blocked, so the forwarded chain's own reply
614 % exits become the reply exits of this entry.
615 fwd = forwardingOf(eidx);
616 if ~isempty(fwd) && ~isempty(replyExits)
617 ownPorts = replyExits;
618 fwdExits = zeros(0, 3);
619 pforw = 0.0;
620 % The forwarder's thread is released at the handoff, so the
621 % forwarded chain is expanded without it on the thread stack.
622 fwdThread = threadStack(end);
623 threadStack(end) = [];
624 for f = 1:size(fwd, 1)
625 [fFirst, fReplies, fTerms] = expandEntry(fwd(f, 1), refTidx);
626 if isempty(fFirst)
627 continue;
628 end
629 p = fwd(f, 2);
630 for r = 1:size(ownPorts, 1)
631 addRoute(ownPorts(r, 1:2), fFirst, ownPorts(r, 3) * p);
632 end
633 pforw = pforw + p;
634 fwdExits = [fwdExits; fReplies; fTerms]; %#ok<AGROW>
635 end
636 threadStack(end+1) = fwdThread;
637 % What is left of each of this entry's own ports still replies.
638 replyExits(:, 3) = replyExits(:, 3) * max(0.0, 1.0 - pforw);
639 replyExits = [replyExits; fwdExits];
640 end
641 end
642
643 function fwd = forwardingOf(eidx)
644 % Rows [targetEidx, probability] of the forwarding calls of an entry.
645 fwd = zeros(0, 2);
646 if ~isfield(lsn, 'calltype') || isempty(lsn.calltype)
647 return;
648 end
649 for cidx = 1:size(lsn.callpair, 1)
650 if full(lsn.calltype(cidx)) ~= CallType.FWD || lsn.callpair(cidx, 1) ~= eidx
651 continue;
652 end
653 p = 1.0;
654 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
655 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx}, 'Distribution')
656 p = lsn.callproc{cidx}.getMean();
657 end
658 p = min(max(p, 0.0), 1.0);
659 if p > GlobalConstants.FineTol
660 fwd(end+1, :) = [lsn.callpair(cidx, 2), p]; %#ok<AGROW>
661 end
662 end
663 end
664
665 function popEntry()
666 entryStack(end) = [];
667 threadStack(end) = [];
668 end
669
670 function [firstStep, replyExits, terminals] = expandActivities(a0, eidx, refTidx)
671 % Walks the intra-task activity graph from a0, creating steps and
672 % expanding every synchronous call site. Reply exits and terminals
673 % are returned as ports, [step, isSignal] rows.
674 firstStep = [];
675 replyExits = zeros(0, 3);
676 terminals = zeros(0, 3);
677
678 tidx = lsn.parent(a0);
679 localActs = lsn.actsof{eidx};
680 visited = containers.Map('KeyType', 'double', 'ValueType', 'any'); % aidx -> [entryStep, exitStep, exitIsSignal]
681 joinOf = containers.Map('KeyType', 'double', 'ValueType', 'any'); % join activity -> [joinStep, entryStep]
682 forkOwnerStack = []; % class-owner step of each enclosing AND-fork
683 sawReply = false;
684
685 [firstStep, ~] = walk(a0);
686
687 % Every end of the chain returns the token to the caller: as a reply
688 % if the entry replies anywhere, otherwise as a plain terminal, which
689 % the caller still has to route somewhere or the token is lost.
690 if sawReply
691 replyExits = [replyExits; terminals]; %#ok<AGROW>
692 terminals = zeros(0, 3);
693 end
694
695 function [entryStep, exitPort] = walk(aidx)
696 if isKey(visited, aidx)
697 se = visited(aidx);
698 entryStep = se(1);
699 exitPort = se(2:3);
700 return;
701 end
702 % The activity bound to an ItemEntry of a CacheTask is the read
703 % step: it sits on the Cache node rather than on the processor.
704 cacheNode = [];
705 if tidx <= length(lsn.iscache) && lsn.iscache(tidx) && full(lsn.graph(eidx, aidx)) > 0
706 cacheNode = getCacheNode(tidx);
707 end
708 [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode);
709 visited(aidx) = [entryStep, exitPort];
710
711 % Reply to the calling entry, if this activity replies. The reply
712 % is not emitted here: an activity may reply and still have
713 % successors (phase 2), and emitting the reply at that point
714 % would add an outgoing route on top of the ones to the
715 % successors. The reply is therefore deferred to the ends of the
716 % chain, which is what makes phase-2 activities run before the
717 % reply rather than after it.
718 repliesHere = false;
719 if isfield(lsn, 'replygraph') && ~isempty(lsn.replygraph)
720 a = aidx - lsn.ashift;
721 e = eidx - lsn.eshift;
722 if a >= 1 && a <= size(lsn.replygraph, 1) && e >= 1 && e <= size(lsn.replygraph, 2)
723 repliesHere = full(lsn.replygraph(a, e));
724 end
725 end
726 if repliesHere
727 sawReply = true;
728 end
729
730 % Local successors within the same entry.
731 succ = find(lsn.graph(aidx, :));
732 succ = succ(ismember(succ, localActs));
733 if isempty(succ)
734 terminals(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
735 return;
736 end
737
738 % Phase 2: the successors of a replying activity run after the
739 % reply. The exit port becomes a reply exit, and each service
740 % completion of its class spawns the continuation at the host
741 % station (sn.classspawn), so the reply token returns to the
742 % caller while the second phase proceeds concurrently. Only the
743 % task's own thread persists through phase 2: the callers have
744 % already been resumed by the reply.
745 % This needs the exit to be a station departure in the step's own
746 % class: a port on a Router or Cache node, or a reply-signal
747 % port, never completes service, so no spawn can hang off it and
748 % phase 2 falls back to running before the reply.
749 if repliesHere
750 ph2Spawn = isempty(cacheNode) && exitPort(2) == 0 && ...
751 isempty(stepNode{exitPort(1)}) && ~isAndFork(succ) && ~isAndJoinPre(aidx);
752 if ~ph2Spawn
753 line_warning(mfilename, sprintf(['Phase-2 activities of %s run ' ...
754 'before the reply: its exit is not a plain station departure.'], ...
755 lsn.names{aidx}));
756 else
757 replyExits(end+1, :) = [exitPort, 1.0]; %#ok<AGROW>
758 savedStack = threadStack;
759 threadStack = tidx;
760 nT0 = size(terminals, 1);
761 posSucc = succ(full(lsn.graph(aidx, succ)) > 0);
762 target = [];
763 if isscalar(posSucc)
764 [sEntry, ~] = walk(posSucc);
765 if isempty(stepNode{sEntry})
766 target = sEntry;
767 end
768 end
769 if isempty(target)
770 % Branching phase 2, or a head on a non-station node:
771 % spawn into an immediate head step that carries the
772 % branch probabilities.
773 head = addStep(aidx, lsn.parent(tidx), [], ...
774 [lsn.names{aidx}, '_ph2'], false, false, refTidx);
775 for s2 = posSucc
776 [sEntry, ~] = walk(s2);
777 addRoute([head, 0], sEntry, full(lsn.graph(aidx, s2)));
778 end
779 target = head;
780 end
781 spawnPairs(end+1, :) = [exitPort(1), target]; %#ok<AGROW>
782 ph2New = terminals(nT0+1:end, :);
783 terminals(nT0+1:end, :) = [];
784 for r2 = 1:size(ph2New, 1)
785 ph2Exits(end+1, :) = [ph2New(r2, 1:3), refTidx]; %#ok<AGROW>
786 end
787 threadStack = savedStack;
788 return;
789 end
790 end
791
792 if ~isempty(cacheNode)
793 % CacheAccess precedence: the successors are the hit branch
794 % and the miss branch, in that order. The Cache node itself
795 % decides which one a job takes, so no probability is put on
796 % these routes.
797 if length(succ) < 2
798 line_warning(mfilename, sprintf(...
799 'Cache read %s has no hit/miss pair; treated as an ordinary activity.', ...
800 lsn.names{aidx}));
801 else
802 [hEntry, ~] = walk(succ(1));
803 [mEntry, ~] = walk(succ(2));
804 addCacheRoute(entryStep, hEntry);
805 addCacheRoute(entryStep, mEntry);
806 cacheWiring(end+1) = struct('node', cacheNode, ...
807 'readStep', entryStep, 'hitStep', hEntry, 'missStep', mEntry, ...
808 'itemproc', lsn.itemproc{eidx}, 'nitems', lsn.nitems(eidx)); %#ok<AGROW>
809 return;
810 end
811 end
812
813 if isAndFork(succ)
814 % AND-fork: the branches run concurrently, so the job is
815 % replicated by a Fork node. One Router per branch, because a
816 % Fork cannot switch class per output link, and the branch
817 % class is what tells the branch apart.
818 forkNode = Fork(model, ['Fork_', lsn.names{aidx}]);
819 forkStep = addAuxStep(forkNode, exitPort(1), ['Fork_', lsn.names{aidx}], refTidx);
820 addRoute(exitPort, forkStep, 1.0);
821 forkOwnerStack(end+1) = forkStep; %#ok<AGROW>
822 for b = 1:length(succ)
823 routerNode = Router(model, sprintf('Fork_%s_%d', lsn.names{aidx}, b));
824 routerStep = addAuxStep(routerNode, forkStep, ...
825 sprintf('Fork_%s_%d', lsn.names{aidx}, b), refTidx);
826 addRoute([forkStep, 0], routerStep, 1.0);
827 [sEntry, ~] = walk(succ(b));
828 addRoute([routerStep, 0], sEntry, 1.0);
829 end
830 forkOwnerStack(end) = [];
831 return;
832 end
833
834 if isAndJoinPre(aidx)
835 % Tail of an AND-fork branch: it must reach the Join, and the
836 % siblings are matched there in the class that entered the
837 % fork, so switch back to it on the way in.
838 joinAidx = succ(1);
839 if isKey(joinOf, joinAidx)
840 js = joinOf(joinAidx);
841 addRoute(exitPort, js(1), 1.0);
842 else
843 if isempty(forkOwnerStack)
844 line_warning(mfilename, sprintf(...
845 'AND-join at %s has no enclosing AND-fork; branches are serialised.', ...
846 lsn.names{joinAidx}));
847 [sEntry, ~] = walk(joinAidx);
848 addRoute(exitPort, sEntry, 1.0);
849 return;
850 end
851 joinNode = Join(model, ['Join_', lsn.names{joinAidx}], stepNode{forkOwnerStack(end)});
852 joinStep = addAuxStep(joinNode, forkOwnerStack(end), ...
853 ['Join_', lsn.names{joinAidx}], refTidx);
854 addRoute(exitPort, joinStep, 1.0);
855 [sEntry, ~] = walk(joinAidx);
856 addRoute([joinStep, 0], sEntry, 1.0);
857 joinOf(joinAidx) = [joinStep, sEntry];
858 end
859 return;
860 end
861
862 for s = succ
863 p = full(lsn.graph(aidx, s));
864 if p <= 0
865 continue;
866 end
867 [sEntry, ~] = walk(s);
868 addRoute(exitPort, sEntry, p);
869 end
870 end
871 end
872
873 function [entryStep, exitPort] = makeActivitySteps(aidx, tidx, refTidx, cacheNode)
874 % One step for the host demand, plus one step per unrolled
875 % synchronous call stage.
876 hidx = lsn.parent(tidx);
877 if nargin >= 4 && ~isempty(cacheNode)
878 % A read step holds no demand and issues no call: the lookup is
879 % instantaneous and the work is done on the hit or miss branch.
880 entryStep = addStep(aidx, hidx, [], lsn.names{aidx}, false, false, refTidx);
881 stepNode{entryStep} = cacheNode;
882 if aidx <= length(lsn.callsof) && ~isempty(lsn.callsof{aidx})
883 line_warning(mfilename, sprintf(...
884 'Calls issued by cache read activity %s are ignored.', lsn.names{aidx}));
885 end
886 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx}, 'Distribution') && ...
887 ~isa(lsn.hostdem{aidx}, 'Immediate') && lsn.hostdem{aidx}.getMean() > GlobalConstants.FineTol
888 line_warning(mfilename, sprintf(...
889 'Host demand of cache read activity %s is ignored.', lsn.names{aidx}));
890 end
891 exitPort = [entryStep, 0];
892 return;
893 end
894 svc = [];
895 if aidx <= length(lsn.hostdem) && isa(lsn.hostdem{aidx}, 'Distribution')
896 d = lsn.hostdem{aidx};
897 if ~isa(d, 'Immediate') && d.getMean() > GlobalConstants.FineTol
898 svc = d;
899 end
900 end
901
902 callStages = synchCallStages(aidx);
903 entryStep = addStep(aidx, hidx, svc, lsn.names{aidx}, false, false, refTidx);
904 % cur is the port through which the activity is currently left. A
905 % blocking call site is left through its reply signal.
906 cur = [entryStep, 0];
907
908 % A call blocks a server only where the caller's host has finitely
909 % many of them. On an INF host there is no server to hold, and a
910 % REPLY signal arriving at a delay finds no pending reply and is
911 % annihilated, so such a call returns by an ordinary class switch.
912 % A thread-pool task also does not hold its server across a call:
913 % the processor is released while its thread waits for the reply,
914 % and the finite capacity region caps its concurrency instead. A
915 % call on an open chain (refTidx 0) never holds either, so that no
916 % REPLY signal, a closed class, is woven into an open chain.
917 hostBlocks = ~hostIsDelay(hidx) && ~fcrTask(tidx) && refTidx ~= 0;
918
919 for k = 1:length(callStages)
920 stage = callStages(k);
921 % An asynchronous call does not hold the caller's server: the
922 % token visits the callee and comes back by a class switch, which
923 % is an external node and so releases the server. It is still
924 % serialised with the caller, which is the approximation.
925 blocks = hostBlocks && ~stage.isasync;
926 % An asynchronous send also releases the caller's thread: the
927 % callee is expanded without the caller on the thread stack, so
928 % the serialised visit does not count against the caller's pool.
929 if stage.isasync
930 asyncThread = threadStack(end);
931 threadStack(end) = [];
932 end
933 [calleeFirst, calleeReplies, calleeTerms] = expandEntry(stage.targetEidx, refTidx);
934 if stage.isasync
935 threadStack(end+1) = asyncThread;
936 end
937 if isempty(calleeFirst)
938 continue; % callee not expandable: drop the call, never block
939 end
940 % A callee path that neither replies nor continues still holds a
941 % token, so it returns to the caller like a reply would.
942 calleeReplies = [calleeReplies; calleeTerms]; %#ok<AGROW>
943
944 % A merge step is needed whenever the call may be skipped, or
945 % whenever a further stage must follow. Where it is not needed
946 % the reply signal itself carries the activity onward, which
947 % matters because a class switch is performed by an external
948 % ClassSwitch node: re-entering the caller's own station would
949 % release its server and admit spurious concurrency.
950 % A branch tail of an AND-join must reach the Join in an
951 % ordinary class: a REPLY signal carries no forked-task
952 % identity, so it would never be matched with its siblings.
953 needsMerge = (stage.prob < 1.0) || (k < length(callStages)) || ...
954 ~blocks || isAndJoinPre(aidx);
955 if needsMerge
956 nxt = addStep(aidx, hidx, [], sprintf('%s_c%d_ret', lsn.names{aidx}, k), false, false, refTidx);
957 if ~blocks
958 % A non-blocking return carries no signal, so the merge
959 % point can sit on a Router: re-entering the caller's
960 % station would queue a zero-work continuation behind
961 % its server.
962 stepNode{nxt} = Router(model, sprintf('%s_c%d_ret', lsn.names{aidx}, k));
963 end
964 end
965
966 if blocks
967 % The first mandatory call is bound to the service class
968 % itself rather than to a separate stage, for the same
969 % reason: an intervening class switch would release the
970 % server between the host demand and the call.
971 if k == 1 && stage.prob >= 1.0 && isequal(cur, [entryStep, 0])
972 blk = entryStep;
973 stepBlocks(blk) = true;
974 else
975 blk = addStep(aidx, hidx, [], sprintf('%s_c%d', lsn.names{aidx}, k), true, false, refTidx);
976 addRoute(cur, blk, stage.prob);
977 if stage.prob < 1.0
978 addRoute(cur, nxt, 1.0 - stage.prob);
979 end
980 end
981 addRoute([blk, 0], calleeFirst, 1.0);
982 for r = 1:size(calleeReplies, 1)
983 reply(end+1, :) = [calleeReplies(r, 1), blk, calleeReplies(r, 2), calleeReplies(r, 3)]; %#ok<AGROW>
984 end
985 if needsMerge
986 addRoute([blk, 1], nxt, 1.0);
987 cur = [nxt, 0];
988 else
989 cur = [blk, 1];
990 end
991 else
992 addRoute(cur, calleeFirst, stage.prob);
993 if stage.prob < 1.0
994 addRoute(cur, nxt, 1.0 - stage.prob);
995 end
996 for r = 1:size(calleeReplies, 1)
997 addRoute(calleeReplies(r, 1:2), nxt, calleeReplies(r, 3));
998 end
999 cur = [nxt, 0];
1000 end
1001 end
1002 exitPort = cur;
1003 end
1004
1005 function cnode = getCacheNode(tidx)
1006 % One Cache node per CacheTask. The name is suffixed so that it does
1007 % not collide with the station of the task's processor.
1008 if isKey(cacheNodeOf, tidx)
1009 cnode = cacheNodeOf(tidx);
1010 return;
1011 end
1012 cnode = Cache(model, [lsn.names{tidx}, '_Cache'], lsn.nitems(tidx), ...
1013 lsn.itemcap{tidx}, lsn.replacestrat(tidx));
1014 cacheNodeOf(tidx) = cnode;
1015 end
1016
1017 function tf = isAndFork(succ)
1018 % An AND-fork is a precedence whose post activities are all marked
1019 % POST_AND: they are entered together, not with a probability each.
1020 tf = false;
1021 if length(succ) < 2 || ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
1022 return;
1023 end
1024 tf = all(arrayfun(@(s) s <= length(lsn.actposttype) && ...
1025 full(lsn.actposttype(s)) == ActivityPrecedenceType.POST_AND, succ));
1026 end
1027
1028 function tf = isAndJoinPre(aidx)
1029 % An activity marked PRE_AND is one branch tail of an AND-join.
1030 tf = isfield(lsn, 'actpretype') && ~isempty(lsn.actpretype) && ...
1031 aidx <= length(lsn.actpretype) && ...
1032 full(lsn.actpretype(aidx)) == ActivityPrecedenceType.PRE_AND;
1033 end
1034
1035 function addRoute(fromPort, toStep, prob)
1036 flow(end+1, :) = [fromPort(1), toStep, prob, fromPort(2), 0]; %#ok<AGROW>
1037 end
1038
1039 function addCacheRoute(fromStep, toStep)
1040 % Leaving a Cache node: the switch into the hit or the miss class is
1041 % made by the node, so the route is declared in the target class.
1042 flow(end+1, :) = [fromStep, toStep, 1.0, 0, 1]; %#ok<AGROW>
1043 end
1044
1045 function stages = synchCallStages(aidx)
1046 % Unrolls the synchronous calls of an activity into blocking stages.
1047 stages = struct('targetEidx', {}, 'prob', {}, 'isasync', {});
1048 if aidx > length(lsn.callsof) || isempty(lsn.callsof{aidx})
1049 return;
1050 end
1051 for cidx = lsn.callsof{aidx}
1052 ctype = full(lsn.calltype(cidx));
1053 if ctype ~= CallType.SYNC && ctype ~= CallType.ASYNC
1054 continue;
1055 end
1056 isasync = (ctype == CallType.ASYNC);
1057 targetEidx = lsn.callpair(cidx, 2);
1058 m = 1.0;
1059 if isfield(lsn, 'callproc') && ~isempty(lsn.callproc) && ...
1060 cidx <= length(lsn.callproc) && isa(lsn.callproc{cidx}, 'Distribution')
1061 m = lsn.callproc{cidx}.getMean();
1062 end
1063 nfull = floor(m + GlobalConstants.FineTol);
1064 frac = m - nfull;
1065 if nfull > MAXCALLSTAGES
1066 line_warning(mfilename, sprintf('Call multiplicity %g on %s truncated to %d stages.', ...
1067 m, lsn.callnames{cidx}, MAXCALLSTAGES));
1068 nfull = MAXCALLSTAGES;
1069 frac = 0;
1070 end
1071 for k = 1:nfull
1072 stages(end+1) = struct('targetEidx', targetEidx, 'prob', 1.0, 'isasync', isasync); %#ok<AGROW>
1073 end
1074 if frac > GlobalConstants.FineTol
1075 stages(end+1) = struct('targetEidx', targetEidx, 'prob', frac, 'isasync', isasync); %#ok<AGROW>
1076 end
1077 end
1078 end
1079
1080 function warnUnsupported(lsn)
1081 if any(full(lsn.calltype) == CallType.ASYNC)
1082 line_warning(mfilename, ['Asynchronous calls are represented by LQN2QN as ' ...
1083 'non-blocking visits: the caller releases its server but remains ' ...
1084 'serialised behind the callee.']);
1085 end
1086 if isfield(lsn, 'hasretrieval') && any(lsn.hasretrieval)
1087 line_warning(mfilename, 'Delayed-hit retrieval on the cache miss path is not represented by LQN2QN.');
1088 end
1089 end
1090
1091 function tf = taskHasAndFork(tidx_)
1092 % True if any activity of the task is the head of an AND-fork
1093 % branch, i.e. is marked POST_AND.
1094 tf = false;
1095 if ~isfield(lsn, 'actposttype') || isempty(lsn.actposttype)
1096 return;
1097 end
1098 for a_ = lsn.ashift+1:lsn.ashift+lsn.nacts
1099 if lsn.parent(a_) == tidx_ && a_ <= length(lsn.actposttype) && ...
1100 full(lsn.actposttype(a_)) == ActivityPrecedenceType.POST_AND
1101 tf = true;
1102 return;
1103 end
1104 end
1105 end
1106
1107 function v = ifempty(x, d)
1108 if isempty(x)
1109 v = d;
1110 else
1111 v = x;
1112 end
1113 end
1114
1115end
Definition fjtag.m:157
Definition Station.m:245