1classdef Workflow < Model
2 % Workflow - A computational workflow that can be converted to a PH distribution.
4 % Workflow allows declaring computational workflows
using the same
5 % activity graph syntax as LayeredNetwork. The workflow can then be
6 % converted to an equivalent phase-type (PH) distribution via
explicit
9 % Supported precedence patterns:
10 % - Serial: Sequential execution
11 % - AndFork/AndJoin: Parallel execution with synchronization
12 % - OrFork/OrJoin: Probabilistic branching
13 % - Loop: Repeated execution
16 % wf = Workflow(
'myWorkflow');
17 % A = wf.addActivity(
'A', Exp.fitMean(1.0));
18 % B = wf.addActivity(
'B', Exp.fitMean(2.0));
19 % wf.addPrecedence(Workflow.Serial(A, B));
22 % Copyright (c) 2012-2026, Imperial College London
23 % All rights reserved.
26 activities = {}; % Cell array of WorkflowActivity objects
27 precedences = []; % Array of ActivityPrecedence objects
28 activityMap; % Map from activity name to index
32 cachedPH; % Cached phase-type distribution result
36 function self = Workflow(name)
37 % WORKFLOW Create a
new Workflow instance
39 % SELF = WORKFLOW(NAME)
42 % name - Name
for the workflow
46 self.precedences = [];
47 self.activityMap = containers.Map();
51 function act = addActivity(self, name, hostDemand)
52 % ADDACTIVITY Add an activity to the workflow
54 % ACT = ADDACTIVITY(SELF, NAME, HOSTDEMAND)
57 % name - Activity name (
string)
58 % hostDemand - Service time distribution or numeric mean
61 % act - WorkflowActivity
object
64 hostDemand = GlobalConstants.FineTol;
67 act = WorkflowActivity(self, name, hostDemand);
68 self.activities{end+1} = act;
69 act.index = length(self.activities);
70 self.activityMap(name) = act.index;
71 self.cachedPH = []; % Invalidate cache
74 function self = addPrecedence(self, prec)
75 % ADDPRECEDENCE Add precedence constraints to the workflow
77 % SELF = ADDPRECEDENCE(SELF, PREC)
80 % prec - ActivityPrecedence
object or cell array (from Serial)
83 for m = 1:length(prec)
84 self.precedences = [self.precedences; prec{m}];
87 self.precedences = [self.precedences; prec];
89 self.cachedPH = []; % Invalidate cache
92 function act = getActivity(self, name)
93 % GETACTIVITY Get activity by name
95 % ACT = GETACTIVITY(SELF, NAME)
98 % name - Activity name (
string)
101 % act - WorkflowActivity
object
103 if isa(name,
'WorkflowActivity')
107 if ~isKey(self.activityMap, name)
108 line_error(mfilename, sprintf('Activity "%s" not found in workflow.', name));
110 idx = self.activityMap(name);
111 act = self.activities{idx};
114 function idx = getActivityIndex(self, name)
115 % GETACTIVITYINDEX Get activity index by name
117 % IDX = GETACTIVITYINDEX(SELF, NAME)
119 if isa(name,
'WorkflowActivity')
121 elseif isKey(self.activityMap, name)
122 idx = self.activityMap(name);
128 function [isValid, msg] = validate(self)
129 % VALIDATE Validate workflow structure
131 % [ISVALID, MSG] = VALIDATE(SELF)
134 % isValid - true if workflow
is valid
135 % msg - Error message if invalid
140 % Check 1: At least one activity
141 if isempty(self.activities)
143 msg = 'Workflow must have at least one activity.';
147 % Check 2: All activities in precedences exist
148 for p = 1:length(self.precedences)
149 prec = self.precedences(p);
150 for a = 1:length(prec.preActs)
151 if ~isKey(self.activityMap, prec.preActs{a})
153 msg = sprintf(
'Activity "%s" referenced in precedence not found in workflow.', prec.preActs{a});
157 for a = 1:length(prec.postActs)
158 if ~isKey(self.activityMap, prec.postActs{a})
160 msg = sprintf(
'Activity "%s" referenced in precedence not found in workflow.', prec.postActs{a});
166 % Check 3: OR-fork probabilities sum to 1
167 for p = 1:length(self.precedences)
168 prec = self.precedences(p);
169 if prec.postType == ActivityPrecedenceType.POST_OR
170 if isempty(prec.postParams)
172 msg = 'OR-fork must have probabilities specified.';
175 if abs(sum(prec.postParams) - 1.0) > GlobalConstants.FineTol
177 msg = sprintf('OR-fork probabilities must sum to 1 (got %.4f).', sum(prec.postParams));
183 % Check 4: Loop counts must be positive
184 for p = 1:length(self.precedences)
185 prec = self.precedences(p);
186 if prec.postType == ActivityPrecedenceType.POST_LOOP
187 if isempty(prec.postParams) || prec.postParams <= 0
189 msg = 'Loop count must be a positive number.';
196 function ph = toPH(self)
197 % TOPH Convert workflow to phase-type distribution
202 % ph - APH distribution representing the workflow execution time
204 if ~isempty(self.cachedPH)
209 [isValid, msg] = self.validate();
211 line_error(mfilename, msg);
214 [alpha, T] = self.buildCTMC();
219 function [alpha, T] = buildCTMC(self)
220 % BUILDCTMC Build the CTMC representation of the workflow
222 % [ALPHA, T] = BUILDCTMC(SELF)
225 % alpha - Initial probability vector
226 % T - Subgenerator matrix
228 % Handle trivial case: single activity
229 if length(self.activities) == 1
230 [alpha, T] = self.activities{1}.getPHRepresentation();
234 % Build adjacency structures from precedences
235 [adjList, inDegree, outDegree, forkInfo, joinInfo, loopInfo] = self.analyzeStructure();
237 % Find start activities (no predecessors in precedence graph)
238 startActs = find(inDegree == 0);
239 if isempty(startActs)
240 % If all activities have predecessors, use first activity
244 % Find end activities (no successors in precedence graph)
245 endActs = find(outDegree == 0);
247 endActs = length(self.activities);
250 % Build the workflow CTMC
using recursive block composition
251 [alpha, T] = self.composeWorkflow(startActs, endActs, adjList, forkInfo, joinInfo, loopInfo);
255 methods (Access =
private)
256 function [adjList, inDegree, outDegree, forkInfo, joinInfo, loopInfo] = analyzeStructure(self)
257 % ANALYZESTRUCTURE Analyze workflow structure from precedences
259 % Build adjacency list and identify fork/join/loop structures
261 n = length(self.activities);
262 adjList = cell(n, 1);
263 inDegree = zeros(n, 1);
264 outDegree = zeros(n, 1);
265 forkInfo = struct('type', {},
'preAct', {},
'postActs', {},
'probs', {});
266 joinInfo =
struct(
'type', {},
'preActs', {},
'postAct', {},
'quorum', {});
267 loopInfo =
struct(
'preAct', {},
'loopActs', {},
'endAct', {},
'count', {});
269 for p = 1:length(self.precedences)
270 prec = self.precedences(p);
272 % Get activity indices
273 preInds = zeros(length(prec.preActs), 1);
274 for a = 1:length(prec.preActs)
275 preInds(a) = self.getActivityIndex(prec.preActs{a});
278 postInds = zeros(length(prec.postActs), 1);
279 for a = 1:length(prec.postActs)
280 postInds(a) = self.getActivityIndex(prec.postActs{a});
283 % Update adjacency and degrees
284 for i = 1:length(preInds)
285 for j = 1:length(postInds)
286 adjList{preInds(i)} = [adjList{preInds(i)}, postInds(j)];
287 outDegree(preInds(i)) = outDegree(preInds(i)) + 1;
288 inDegree(postInds(j)) = inDegree(postInds(j)) + 1;
292 % Record fork/join/loop info
294 case ActivityPrecedenceType.POST_AND
296 forkInfo(end+1) =
struct(
'type',
'and', ...
297 'preAct', preInds(1), ...
298 'postActs', postInds(:)
', ...
301 case ActivityPrecedenceType.POST_OR
303 forkInfo(end+1) = struct('type
', 'or
', ...
304 'preAct
', preInds(1), ...
305 'postActs
', postInds(:)', ...
306 'probs', prec.postParams(:)
');
308 case ActivityPrecedenceType.POST_LOOP
310 count = prec.postParams;
311 if length(postInds) >= 2
312 loopInfo(end+1) = struct('preAct
', preInds(1), ...
313 'loopActs
', postInds(1:end-1)', ...
314 'endAct', postInds(end), ...
317 loopInfo(end+1) =
struct(
'preAct', preInds(1), ...
318 'loopActs', postInds(1)
', ...
325 case ActivityPrecedenceType.PRE_AND
327 quorum = prec.preParams;
329 quorum = length(preInds);
331 joinInfo(end+1) = struct('type
', 'and
', ...
332 'preActs
', preInds(:)', ...
333 'postAct', postInds(1), ...
336 case ActivityPrecedenceType.PRE_OR
338 joinInfo(end+1) =
struct(
'type',
'or', ...
339 'preActs', preInds(:)
', ...
340 'postAct
', postInds(1), ...
346 function [alpha, T] = composeWorkflow(self, startActs, endActs, adjList, forkInfo, joinInfo, loopInfo)
347 % COMPOSEWORKFLOW Compose the workflow CTMC
349 % Uses a simplified approach: process precedences in order,
350 % building up the CTMC incrementally.
352 n = length(self.activities);
354 % Handle special cases
356 [alpha, T] = self.activities{1}.getPHRepresentation();
360 % Determine workflow structure and compose accordingly
361 % Check for simple serial workflow (no forks/joins/loops)
362 if isempty(forkInfo) && isempty(joinInfo) && isempty(loopInfo)
363 [alpha, T] = self.composeSerialWorkflow(adjList, startActs);
367 % For complex workflows, build block-by-block
368 [alpha, T] = self.composeComplexWorkflow(adjList, forkInfo, joinInfo, loopInfo);
371 function [alpha, T] = composeSerialWorkflow(self, adjList, startActs)
372 % COMPOSESERIALWORKFLOW Compose a purely serial workflow
374 % Activities are concatenated in topological order.
376 n = length(self.activities);
379 order = self.topologicalSort(adjList);
382 [alpha, T] = self.activities{order(1)}.getPHRepresentation();
384 for i = 2:length(order)
385 [alpha_i, T_i] = self.activities{order(i)}.getPHRepresentation();
386 [alpha, T] = Workflow.composeSerial(alpha, T, alpha_i, T_i);
390 function order = topologicalSort(self, adjList)
391 % TOPOLOGICALSORT Perform topological sort on activity graph
393 n = length(self.activities);
396 % Calculate in-degrees
399 inDeg(j) = inDeg(j) + 1;
403 % BFS-based topological sort
404 queue = find(inDeg == 0);
406 queue = 1; % Start with first activity if no clear start
410 while ~isempty(queue)
413 order = [order, curr];
415 for next = adjList{curr}
416 inDeg(next) = inDeg(next) - 1;
418 queue = [queue, next];
423 % Add any remaining activities not in precedence graph
424 remaining = setdiff(1:n, order);
425 order = [order, remaining];
428 function [alpha, T] = composeComplexWorkflow(self, adjList, forkInfo, joinInfo, loopInfo)
429 % COMPOSECOMPLEXWORKFLOW Compose workflow with forks/joins/loops
431 % Strategy: Build a stage-based representation where each stage
432 % is either a single activity, a loop block, or a fork-join block.
433 % Stages are then composed in topological order.
435 % The key insight is that fork-join creates parallel branches that
436 % need Kronecker sum expansion for proper state space modeling.
437 % The map_max structure is used for join synchronization:
438 % [krons(A,B), trans_to_onlyA, trans_to_onlyB]
442 n = length(self.activities);
444 % Build a representation where each activity or block has a PH
445 blockAlpha = cell(n, 1);
448 % blockRepresentative(i) = j means activity i's result
is stored in block j
449 % This handles the case where multiple activities are merged into one block
450 blockRepresentative = (1:n)
';
452 % isBlockHead(i) = true means block i should be included in final composition
453 isBlockHead = true(n, 1);
455 % Initialize each activity as its own block
457 [blockAlpha{i}, blockT{i}] = self.activities{i}.getPHRepresentation();
460 % Process loops first (they modify the activity's PH)
461 for k = 1:length(loopInfo)
463 preIdx = loop.preAct;
464 loopActInds = loop.loopActs;
465 endIdx = loop.endAct;
469 if length(loopActInds) == 1
470 [alpha_loop, T_loop] = self.activities{loopActInds(1)}.getPHRepresentation();
472 % Serial composition of loop activities
473 [alpha_loop, T_loop] = self.activities{loopActInds(1)}.getPHRepresentation();
474 for j = 2:length(loopActInds)
475 [a_j, T_j] = self.activities{loopActInds(j)}.getPHRepresentation();
476 [alpha_loop, T_loop] = Workflow.composeSerial(alpha_loop, T_loop, a_j, T_j);
480 % Create count-fold convolution
481 [alpha_conv, T_conv] = Workflow.composeRepeat(alpha_loop, T_loop, count);
483 % Compose with pre-activity
484 [alpha_result, T_result] = Workflow.composeSerial(...
485 blockAlpha{preIdx}, blockT{preIdx}, alpha_conv, T_conv);
487 % Compose with end activity
if present
489 [alpha_end, T_end] = self.activities{endIdx}.getPHRepresentation();
490 [alpha_result, T_result] = Workflow.composeSerial(alpha_result, T_result, alpha_end, T_end);
491 % Merge end activity into preIdx block
492 blockRepresentative(endIdx) = preIdx;
493 isBlockHead(endIdx) =
false;
496 blockAlpha{preIdx} = alpha_result;
497 blockT{preIdx} = T_result;
499 % Merge loop activities into preIdx block
500 for j = 1:length(loopActInds)
501 blockRepresentative(loopActInds(j)) = preIdx;
502 isBlockHead(loopActInds(j)) =
false;
506 % Process AND-forks and their matching AND-joins
507 % These create parallel blocks with Kronecker sum state expansion
508 for k = 1:length(forkInfo)
510 if strcmp(fork.type,
'and')
511 % Find matching AND-join
512 joinIdx = self.findMatchingJoin(fork.postActs, joinInfo,
'and');
515 join = joinInfo(joinIdx);
516 preIdx = fork.preAct;
517 postIdx = join.postAct;
518 parallelInds = fork.postActs;
520 % Compose parallel activities
using Kronecker sum (map_max structure)
521 [alpha_par, T_par] = self.composeAndForkBlock(parallelInds, blockAlpha, blockT);
523 % Store parallel block result - DO NOT compose with pre/post here
524 % The pre/post connections will be handled by topological composition
526 % Create a
new "virtual" block
for the parallel section
527 % We store it in the first parallel activity
's slot
528 firstParallel = parallelInds(1);
529 blockAlpha{firstParallel} = alpha_par;
530 blockT{firstParallel} = T_par;
532 % Mark other parallel activities as merged into firstParallel
533 for j = 2:length(parallelInds)
534 blockRepresentative(parallelInds(j)) = firstParallel;
535 isBlockHead(parallelInds(j)) = false;
538 % Update adjacency: preIdx -> firstParallel -> postIdx
539 % The fork and join activities keep their blocks separate
544 % Process OR-forks and their matching OR-joins
545 for k = 1:length(forkInfo)
547 if strcmp(fork.type, 'or
')
548 % Find matching OR-join
549 joinIdx = self.findMatchingJoin(fork.postActs, joinInfo, 'or
');
551 branchInds = fork.postActs;
554 % Compose branches as probabilistic mixture
555 [alpha_or, T_or] = self.composeOrForkBlock(branchInds, probs, blockAlpha, blockT);
557 % Store in first branch's slot
558 firstBranch = branchInds(1);
559 blockAlpha{firstBranch} = alpha_or;
560 blockT{firstBranch} = T_or;
562 % Mark other branches as merged
563 for j = 2:length(branchInds)
564 blockRepresentative(branchInds(j)) = firstBranch;
565 isBlockHead(branchInds(j)) =
false;
570 % Build modified adjacency list that respects block merging
571 modAdjList = cell(n, 1);
574 % Find successors, mapping through representatives
576 for succ = adjList{i}
577 rep = blockRepresentative(succ);
578 if rep ~= i && isBlockHead(rep) && ~ismember(rep, successors)
579 successors = [successors, rep];
582 modAdjList{i} = successors;
586 % Topological sort on block heads only
587 order = self.topologicalSortBlocks(modAdjList, isBlockHead);
589 % Compose blocks in topological order
593 for i = 1:length(order)
596 alpha = blockAlpha{idx};
599 [alpha, T] = Workflow.composeSerial(alpha, T, blockAlpha{idx}, blockT{idx});
603 % If still empty (shouldn
't happen), use first activity
605 [alpha, T] = self.activities{1}.getPHRepresentation();
609 function order = topologicalSortBlocks(self, adjList, isBlockHead)
610 % TOPOLOGICALSORTBLOCKS Topological sort considering only block heads
612 n = length(self.activities);
615 % Calculate in-degrees for block heads only
620 inDeg(j) = inDeg(j) + 1;
626 % Find starting nodes (block heads with in-degree 0)
629 if isBlockHead(i) && inDeg(i) == 0
635 % Fallback: use first block head
645 while ~isempty(queue)
648 order = [order, curr];
650 for next = adjList{curr}
652 inDeg(next) = inDeg(next) - 1;
654 queue = [queue, next];
660 % Add any remaining block heads not reached
662 if isBlockHead(i) && ~ismember(i, order)
668 function joinIdx = findMatchingJoin(self, postActs, joinInfo, joinType)
669 % FINDMATCHINGJOIN Find the join that matches a fork's post activities
672 for j = 1:length(joinInfo)
674 if strcmp(join.type, joinType)
675 % Check
if this join
's pre-activities match the fork's post-activities
676 if isequal(sort(join.preActs), sort(postActs))
684 function [alpha, T] = composeAndForkBlock(self, parallelInds, blockAlpha, blockT)
685 % COMPOSEANDFORKBLOCK Compose parallel activities
using Kronecker sum
687 % Start with first parallel activity
688 alpha = blockAlpha{parallelInds(1)};
689 T = blockT{parallelInds(1)};
691 % Combine with remaining activities
using Kronecker sum
692 for i = 2:length(parallelInds)
693 idx = parallelInds(i);
694 alpha_i = blockAlpha{idx};
697 [alpha, T] = Workflow.composeParallel(alpha, T, alpha_i, T_i);
701 function [alpha, T] = composeOrForkBlock(self, branchInds, probs, blockAlpha, blockT)
702 % COMPOSEORFORKBLOCK Compose branches as probabilistic mixture
704 % Calculate total phases
706 for i = 1:length(branchInds)
707 totalPhases = totalPhases + size(blockT{branchInds(i)}, 1);
710 T = zeros(totalPhases);
711 alpha = zeros(1, totalPhases);
714 for i = 1:length(branchInds)
716 alpha_i = blockAlpha{idx};
720 T(offset+1:offset+n_i, offset+1:offset+n_i) = T_i;
721 alpha(offset+1:offset+n_i) = probs(i) * alpha_i;
723 offset = offset + n_i;
729 function [alpha_out, T_out] = composeSerial(alpha1, T1, alpha2, T2)
730 % COMPOSESERIAL Serial composition of two PH distributions
732 % The second PH starts when the first one absorbs.
734 % T_serial = [T1, -T1*e*alpha2]
741 absRate1 = -T1 * e1; % Absorption rate from each state
743 T_out = zeros(n1 + n2);
744 T_out(1:n1, 1:n1) = T1;
745 T_out(1:n1, n1+1:end) = absRate1 * alpha2;
746 T_out(n1+1:end, n1+1:end) = T2;
748 alpha_out = [alpha1, zeros(1, n2)];
751 function [alpha_out, T_out] = composeParallel(alpha1, T1, alpha2, T2)
752 % COMPOSEPARALLEL Parallel fork-join composition
754 % Models two PH distributions running in parallel with synchronization
755 % at completion (AND-join). The resulting PH represents the time until
756 % BOTH activities complete (i.e., the maximum).
759 % - States (i,j) where both are active: i=1..n1, j=1..n2
760 % - States where only activity 1
is active (2 completed): i=1..n1
761 % - States where only activity 2
is active (1 completed): j=1..n2
763 % Absorption occurs only when both have completed.
770 % Absorption rates
for each activity
771 absRate1 = -T1 * e1; % Rate of leaving each phase in activity 1
772 absRate2 = -T2 * e2; % Rate of leaving each phase in activity 2
774 % Total state space: n1*n2 (both active) + n1 (only 1 active) + n2 (only 2 active)
775 % States 1..n1*n2: both active, indexed as (i-1)*n2 + j
for phase (i,j)
776 % States n1*n2+1..n1*n2+n1: only activity 1 active (activity 2 completed)
777 % States n1*n2+n1+1..n1*n2+n1+n2: only activity 2 active (activity 1 completed)
782 nTotal = nBoth + nOnly1 + nOnly2;
784 T_out = zeros(nTotal);
786 % Transitions within
"both active" states
787 % Use Kronecker sum
for simultaneous evolution
788 T_both = krons(T1, T2);
789 T_out(1:nBoth, 1:nBoth) = T_both;
791 % Transitions from
"both active" to
"only 1 active" (activity 2 completes)
792 % When in state (i,j), activity 2 can complete with rate absRate2(j)
793 % This moves to state
"only 1 active, phase i"
796 bothIdx = (i-1)*n2 + j;
797 only1Idx = nBoth + i;
798 T_out(bothIdx, only1Idx) = T_out(bothIdx, only1Idx) + absRate2(j);
802 % Transitions from
"both active" to
"only 2 active" (activity 1 completes)
803 % When in state (i,j), activity 1 can complete with rate absRate1(i)
804 % This moves to state
"only 2 active, phase j"
807 bothIdx = (i-1)*n2 + j;
808 only2Idx = nBoth + nOnly1 + j;
809 T_out(bothIdx, only2Idx) = T_out(bothIdx, only2Idx) + absRate1(i);
813 % Transitions within
"only 1 active" states
814 T_out(nBoth+1:nBoth+nOnly1, nBoth+1:nBoth+nOnly1) = T1;
816 % Transitions within
"only 2 active" states
817 T_out(nBoth+nOnly1+1:end, nBoth+nOnly1+1:end) = T2;
819 % Absorption from
"only 1 active" and
"only 2 active" states happens
820 % when the remaining activity completes -
this is handled by the
821 % negative row sums (implicit absorption)
823 % Initial probability: start in
"both active" states
824 % with probability alpha1(i) * alpha2(j)
for state (i,j)
825 alpha_out = zeros(1, nTotal);
828 bothIdx = (i-1)*n2 + j;
829 alpha_out(bothIdx) = alpha1(i) * alpha2(j);
834 function [alpha_out, T_out] = composeRepeat(alpha, T, count)
835 % COMPOSEREPEAT Repeat a PH distribution count times (convolution)
837 % Equivalent to serial composition of the same PH count times.
841 T_out = -1e10; % Immediate
851 % Use map_sum
for efficient convolution
if available
852 % Otherwise, compose serially
854 % Convert to MAP format
861 % Use map_sum
for count-fold convolution
862 MAP_conv = map_sum(MAP, count);
865 D1_conv = MAP_conv{2};
867 % Extract alpha from D1
868 n_out = size(T_out, 1);
869 e_out = ones(n_out, 1);
870 absRate = -T_out * e_out;
871 idx = find(absRate > GlobalConstants.FineTol, 1);
873 alpha_out = D1_conv(idx, :) / absRate(idx);
875 alpha_out = zeros(1, n_out);
879 % Fallback: serial composition
883 [alpha_out, T_out] = Workflow.composeSerial(alpha_out, T_out, alpha, T);
888 % Static precedence factory methods (delegate to ActivityPrecedence)
889 function ap = Serial(varargin)
890 % SERIAL Create serial precedence
892 % AP = WORKFLOW.SERIAL(A1, A2, ...)
894 % Convert WorkflowActivity to names
895 args = cell(1, nargin);
897 if isa(varargin{i},
'WorkflowActivity')
898 args{i} = varargin{i}.name;
900 args{i} = varargin{i};
903 ap = ActivityPrecedence.Serial(args{:});
906 function ap = AndFork(preAct, postActs)
907 % ANDFORK Create AND-fork precedence
909 % AP = WORKFLOW.ANDFORK(PREACT, {POSTACT1, POSTACT2, ...})
911 if isa(preAct,
'WorkflowActivity')
912 preAct = preAct.name;
914 for a = 1:length(postActs)
915 if isa(postActs{a},
'WorkflowActivity')
916 postActs{a} = postActs{a}.name;
919 ap = ActivityPrecedence.AndFork(preAct, postActs);
922 function ap = AndJoin(preActs, postAct, quorum)
923 % ANDJOIN Create AND-join precedence
925 % AP = WORKFLOW.ANDJOIN({PREACT1, PREACT2, ...}, POSTACT)
926 % AP = WORKFLOW.ANDJOIN({PREACT1, PREACT2, ...}, POSTACT, QUORUM)
928 for a = 1:length(preActs)
929 if isa(preActs{a},
'WorkflowActivity')
930 preActs{a} = preActs{a}.name;
933 if isa(postAct,
'WorkflowActivity')
934 postAct = postAct.name;
939 ap = ActivityPrecedence.AndJoin(preActs, postAct, quorum);
942 function ap = OrFork(preAct, postActs, probs)
943 % ORFORK Create OR-fork (probabilistic) precedence
945 % AP = WORKFLOW.ORFORK(PREACT, {POSTACT1, POSTACT2, ...}, [P1, P2, ...])
947 if isa(preAct,
'WorkflowActivity')
948 preAct = preAct.name;
950 for a = 1:length(postActs)
951 if isa(postActs{a},
'WorkflowActivity')
952 postActs{a} = postActs{a}.name;
955 ap = ActivityPrecedence.OrFork(preAct, postActs, probs);
958 function ap = Xor(preAct, postActs, probs)
959 % XOR Create XOR precedence (alias
for OrFork)
961 % AP = WORKFLOW.XOR(PREACT, {POSTACT1, POSTACT2, ...}, [P1, P2, ...])
963 ap = Workflow.OrFork(preAct, postActs, probs);
966 function ap = OrJoin(preActs, postAct)
967 % ORJOIN Create OR-join precedence
969 % AP = WORKFLOW.ORJOIN({PREACT1, PREACT2, ...}, POSTACT)
971 for a = 1:length(preActs)
972 if isa(preActs{a},
'WorkflowActivity')
973 preActs{a} = preActs{a}.name;
976 if isa(postAct,
'WorkflowActivity')
977 postAct = postAct.name;
979 ap = ActivityPrecedence.OrJoin(preActs, postAct);
982 function ap = Loop(preAct, postActs, counts)
983 % LOOP Create loop precedence
985 % AP = WORKFLOW.LOOP(PREACT, {LOOPACT, ENDACT}, COUNT)
986 % AP = WORKFLOW.LOOP(PREACT, LOOPACT, ENDACT, COUNT)
988 if isa(preAct,
'WorkflowActivity')
989 preAct = preAct.name;
992 for a = 1:length(postActs)
993 if isa(postActs{a},
'WorkflowActivity')
994 postActs{a} = postActs{a}.name;
997 elseif isa(postActs,
'WorkflowActivity')
998 postActs = postActs.name;
1000 ap = ActivityPrecedence.Loop(preAct, postActs, counts);
1003 function wf = fromWfCommons(jsonFile, options)
1004 % FROMWFCOMMONS Load a workflow from a WfCommons JSON file.
1006 % WF = WORKFLOW.FROMWFCOMMONS(JSONFILE)
1007 % WF = WORKFLOW.FROMWFCOMMONS(JSONFILE, OPTIONS)
1009 % Loads a workflow trace from the WfCommons format
1011 % LINE Workflow
object for queueing analysis.
1014 % jsonFile - Path to WfCommons JSON file
1015 % options - Optional struct with:
1016 % .distributionType - 'exp' (default), 'det', '
aph', 'hyperexp'
1017 % .defaultSCV - Default SCV for APH/HyperExp (default: 1.0)
1018 % .defaultRuntime - Default runtime when missing (default: 1.0)
1019 % .useExecutionData - Use execution data if available (default: true)
1020 % .storeMetadata - Store WfCommons metadata (default: true)
1023 % wf - Workflow
object
1026 % wf = Workflow.fromWfCommons('montage-workflow.json');
1028 % fprintf('Mean execution time: %.2f\n', ph.getMean());
1033 wf = WfCommonsLoader.load(jsonFile, options);