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: Identify matched fork-join pairs and compose them
432 % as blocks, then combine blocks serially.
434 n = length(self.activities);
436 % Build a representation where each activity or block has a PH
438 blockAlpha = cell(n, 1);
440 isProcessed = false(n, 1);
442 % Initialize each activity as its own block
444 [blockAlpha{i}, blockT{i}] = self.activities{i}.getPHRepresentation();
447 % Process loops first (they modify the activity's PH)
448 for k = 1:length(loopInfo)
450 preIdx = loop.preAct;
451 loopActInds = loop.loopActs;
452 endIdx = loop.endAct;
456 if length(loopActInds) == 1
457 [alpha_loop, T_loop] = self.activities{loopActInds(1)}.getPHRepresentation();
459 % Serial composition of loop activities
460 [alpha_loop, T_loop] = self.activities{loopActInds(1)}.getPHRepresentation();
461 for j = 2:length(loopActInds)
462 [a_j, T_j] = self.activities{loopActInds(j)}.getPHRepresentation();
463 [alpha_loop, T_loop] = Workflow.composeSerial(alpha_loop, T_loop, a_j, T_j);
467 % Create count-fold convolution
468 [alpha_conv, T_conv] = Workflow.composeRepeat(alpha_loop, T_loop, count);
470 % Compose with pre-activity
471 alpha_pre = blockAlpha{preIdx};
472 T_pre = blockT{preIdx};
473 [alpha_result, T_result] = Workflow.composeSerial(alpha_pre, T_pre, alpha_conv, T_conv);
475 % Compose with end activity
if present
477 [alpha_end, T_end] = self.activities{endIdx}.getPHRepresentation();
478 [alpha_result, T_result] = Workflow.composeSerial(alpha_result, T_result, alpha_end, T_end);
479 isProcessed(endIdx) =
true;
482 blockAlpha{preIdx} = alpha_result;
483 blockT{preIdx} = T_result;
484 isProcessed(preIdx) =
true;
485 isProcessed(loopActInds) =
true;
488 % Process AND-forks and their matching AND-joins
489 for k = 1:length(forkInfo)
491 if strcmp(fork.type,
'and')
492 % Find matching AND-join
493 joinIdx = self.findMatchingJoin(fork.postActs, joinInfo,
'and');
496 join = joinInfo(joinIdx);
497 preIdx = fork.preAct;
498 postIdx = join.postAct;
499 parallelInds = fork.postActs;
501 % Compose parallel activities
502 [alpha_par, T_par] = self.composeAndForkBlock(parallelInds, blockAlpha, blockT);
504 % Compose: pre -> parallel -> post
505 if ~isProcessed(preIdx)
506 [alpha_result, T_result] = Workflow.composeSerial(...
507 blockAlpha{preIdx}, blockT{preIdx}, alpha_par, T_par);
509 alpha_result = alpha_par;
513 if ~isProcessed(postIdx)
514 [alpha_result, T_result] = Workflow.composeSerial(...
515 alpha_result, T_result, blockAlpha{postIdx}, blockT{postIdx});
518 % Store result in pre-activity
's block
519 blockAlpha{preIdx} = alpha_result;
520 blockT{preIdx} = T_result;
523 isProcessed(preIdx) = true;
524 isProcessed(parallelInds) = true;
525 isProcessed(postIdx) = true;
530 % Process OR-forks and their matching OR-joins
531 for k = 1:length(forkInfo)
533 if strcmp(fork.type, 'or
')
534 % Find matching OR-join
535 joinIdx = self.findMatchingJoin(fork.postActs, joinInfo, 'or
');
537 preIdx = fork.preAct;
538 branchInds = fork.postActs;
541 % Compose branches as probabilistic mixture
542 [alpha_or, T_or] = self.composeOrForkBlock(branchInds, probs, blockAlpha, blockT);
544 % Compose: pre -> or-block
545 if ~isProcessed(preIdx)
546 [alpha_result, T_result] = Workflow.composeSerial(...
547 blockAlpha{preIdx}, blockT{preIdx}, alpha_or, T_or);
549 alpha_result = alpha_or;
553 % If there's a join, compose with post activity
555 join = joinInfo(joinIdx);
556 postIdx = join.postAct;
557 if ~isProcessed(postIdx)
558 [alpha_result, T_result] = Workflow.composeSerial(...
559 alpha_result, T_result, blockAlpha{postIdx}, blockT{postIdx});
560 isProcessed(postIdx) =
true;
564 blockAlpha{preIdx} = alpha_result;
565 blockT{preIdx} = T_result;
566 isProcessed(preIdx) =
true;
567 isProcessed(branchInds) =
true;
571 % Compose remaining unprocessed activities in topological order
572 order = self.topologicalSort(adjList);
574 % Find first unprocessed or block-start activity
578 for i = 1:length(order)
580 if ~isProcessed(idx) || (i == 1 && isempty(alpha))
582 alpha = blockAlpha{idx};
585 [alpha, T] = Workflow.composeSerial(alpha, T, blockAlpha{idx}, blockT{idx});
587 elseif isProcessed(idx) && isempty(alpha)
588 % This
is a processed block, use it as starting point
589 alpha = blockAlpha{idx};
594 % If still empty (shouldn
't happen), use first activity
596 [alpha, T] = self.activities{1}.getPHRepresentation();
600 function joinIdx = findMatchingJoin(self, postActs, joinInfo, joinType)
601 % FINDMATCHINGJOIN Find the join that matches a fork's post activities
604 for j = 1:length(joinInfo)
606 if strcmp(join.type, joinType)
607 % Check
if this join
's pre-activities match the fork's post-activities
608 if isequal(sort(join.preActs), sort(postActs))
616 function [alpha, T] = composeAndForkBlock(self, parallelInds, blockAlpha, blockT)
617 % COMPOSEANDFORKBLOCK Compose parallel activities
using Kronecker sum
619 % Start with first parallel activity
620 alpha = blockAlpha{parallelInds(1)};
621 T = blockT{parallelInds(1)};
623 % Combine with remaining activities
using Kronecker sum
624 for i = 2:length(parallelInds)
625 idx = parallelInds(i);
626 alpha_i = blockAlpha{idx};
629 [alpha, T] = Workflow.composeParallel(alpha, T, alpha_i, T_i);
633 function [alpha, T] = composeOrForkBlock(self, branchInds, probs, blockAlpha, blockT)
634 % COMPOSEORFORKBLOCK Compose branches as probabilistic mixture
636 % Calculate total phases
638 for i = 1:length(branchInds)
639 totalPhases = totalPhases + size(blockT{branchInds(i)}, 1);
642 T = zeros(totalPhases);
643 alpha = zeros(1, totalPhases);
646 for i = 1:length(branchInds)
648 alpha_i = blockAlpha{idx};
652 T(offset+1:offset+n_i, offset+1:offset+n_i) = T_i;
653 alpha(offset+1:offset+n_i) = probs(i) * alpha_i;
655 offset = offset + n_i;
661 function [alpha_out, T_out] = composeSerial(alpha1, T1, alpha2, T2)
662 % COMPOSESERIAL Serial composition of two PH distributions
664 % The second PH starts when the first one absorbs.
666 % T_serial = [T1, -T1*e*alpha2]
673 absRate1 = -T1 * e1; % Absorption rate from each state
675 T_out = zeros(n1 + n2);
676 T_out(1:n1, 1:n1) = T1;
677 T_out(1:n1, n1+1:end) = absRate1 * alpha2;
678 T_out(n1+1:end, n1+1:end) = T2;
680 alpha_out = [alpha1, zeros(1, n2)];
683 function [alpha_out, T_out] = composeParallel(alpha1, T1, alpha2, T2)
684 % COMPOSEPARALLEL Parallel fork-join composition
686 % Models two PH distributions running in parallel with synchronization
687 % at completion (AND-join). The resulting PH represents the time until
688 % BOTH activities complete (i.e., the maximum).
691 % - States (i,j) where both are active: i=1..n1, j=1..n2
692 % - States where only activity 1
is active (2 completed): i=1..n1
693 % - States where only activity 2
is active (1 completed): j=1..n2
695 % Absorption occurs only when both have completed.
702 % Absorption rates
for each activity
703 absRate1 = -T1 * e1; % Rate of leaving each phase in activity 1
704 absRate2 = -T2 * e2; % Rate of leaving each phase in activity 2
706 % Total state space: n1*n2 (both active) + n1 (only 1 active) + n2 (only 2 active)
707 % States 1..n1*n2: both active, indexed as (i-1)*n2 + j
for phase (i,j)
708 % States n1*n2+1..n1*n2+n1: only activity 1 active (activity 2 completed)
709 % States n1*n2+n1+1..n1*n2+n1+n2: only activity 2 active (activity 1 completed)
714 nTotal = nBoth + nOnly1 + nOnly2;
716 T_out = zeros(nTotal);
718 % Transitions within
"both active" states
719 % Use Kronecker sum
for simultaneous evolution
720 T_both = krons(T1, T2);
721 T_out(1:nBoth, 1:nBoth) = T_both;
723 % Transitions from
"both active" to
"only 1 active" (activity 2 completes)
724 % When in state (i,j), activity 2 can complete with rate absRate2(j)
725 % This moves to state
"only 1 active, phase i"
728 bothIdx = (i-1)*n2 + j;
729 only1Idx = nBoth + i;
730 T_out(bothIdx, only1Idx) = T_out(bothIdx, only1Idx) + absRate2(j);
734 % Transitions from
"both active" to
"only 2 active" (activity 1 completes)
735 % When in state (i,j), activity 1 can complete with rate absRate1(i)
736 % This moves to state
"only 2 active, phase j"
739 bothIdx = (i-1)*n2 + j;
740 only2Idx = nBoth + nOnly1 + j;
741 T_out(bothIdx, only2Idx) = T_out(bothIdx, only2Idx) + absRate1(i);
745 % Transitions within
"only 1 active" states
746 T_out(nBoth+1:nBoth+nOnly1, nBoth+1:nBoth+nOnly1) = T1;
748 % Transitions within
"only 2 active" states
749 T_out(nBoth+nOnly1+1:end, nBoth+nOnly1+1:end) = T2;
751 % Absorption from
"only 1 active" and
"only 2 active" states happens
752 % when the remaining activity completes -
this is handled by the
753 % negative row sums (implicit absorption)
755 % Initial probability: start in
"both active" states
756 % with probability alpha1(i) * alpha2(j)
for state (i,j)
757 alpha_out = zeros(1, nTotal);
760 bothIdx = (i-1)*n2 + j;
761 alpha_out(bothIdx) = alpha1(i) * alpha2(j);
766 function [alpha_out, T_out] = composeRepeat(alpha, T, count)
767 % COMPOSEREPEAT Repeat a PH distribution count times (convolution)
769 % Equivalent to serial composition of the same PH count times.
773 T_out = -1e10; % Immediate
783 % Use map_sum
for efficient convolution
if available
784 % Otherwise, compose serially
786 % Convert to MAP format
793 % Use map_sum
for count-fold convolution
794 MAP_conv = map_sum(MAP, count);
797 D1_conv = MAP_conv{2};
799 % Extract alpha from D1
800 n_out = size(T_out, 1);
801 e_out = ones(n_out, 1);
802 absRate = -T_out * e_out;
803 idx = find(absRate > GlobalConstants.FineTol, 1);
805 alpha_out = D1_conv(idx, :) / absRate(idx);
807 alpha_out = zeros(1, n_out);
811 % Fallback: serial composition
815 [alpha_out, T_out] = Workflow.composeSerial(alpha_out, T_out, alpha, T);
820 % Static precedence factory methods (delegate to ActivityPrecedence)
821 function ap = Serial(varargin)
822 % SERIAL Create serial precedence
824 % AP = WORKFLOW.SERIAL(A1, A2, ...)
826 % Convert WorkflowActivity to names
827 args = cell(1, nargin);
829 if isa(varargin{i},
'WorkflowActivity')
830 args{i} = varargin{i}.name;
832 args{i} = varargin{i};
835 ap = ActivityPrecedence.Serial(args{:});
838 function ap = AndFork(preAct, postActs)
839 % ANDFORK Create AND-fork precedence
841 % AP = WORKFLOW.ANDFORK(PREACT, {POSTACT1, POSTACT2, ...})
843 if isa(preAct,
'WorkflowActivity')
844 preAct = preAct.name;
846 for a = 1:length(postActs)
847 if isa(postActs{a},
'WorkflowActivity')
848 postActs{a} = postActs{a}.name;
851 ap = ActivityPrecedence.AndFork(preAct, postActs);
854 function ap = AndJoin(preActs, postAct, quorum)
855 % ANDJOIN Create AND-join precedence
857 % AP = WORKFLOW.ANDJOIN({PREACT1, PREACT2, ...}, POSTACT)
858 % AP = WORKFLOW.ANDJOIN({PREACT1, PREACT2, ...}, POSTACT, QUORUM)
860 for a = 1:length(preActs)
861 if isa(preActs{a},
'WorkflowActivity')
862 preActs{a} = preActs{a}.name;
865 if isa(postAct,
'WorkflowActivity')
866 postAct = postAct.name;
871 ap = ActivityPrecedence.AndJoin(preActs, postAct, quorum);
874 function ap = OrFork(preAct, postActs, probs)
875 % ORFORK Create OR-fork (probabilistic) precedence
877 % AP = WORKFLOW.ORFORK(PREACT, {POSTACT1, POSTACT2, ...}, [P1, P2, ...])
879 if isa(preAct,
'WorkflowActivity')
880 preAct = preAct.name;
882 for a = 1:length(postActs)
883 if isa(postActs{a},
'WorkflowActivity')
884 postActs{a} = postActs{a}.name;
887 ap = ActivityPrecedence.OrFork(preAct, postActs, probs);
890 function ap = Xor(preAct, postActs, probs)
891 % XOR Create XOR precedence (alias
for OrFork)
893 % AP = WORKFLOW.XOR(PREACT, {POSTACT1, POSTACT2, ...}, [P1, P2, ...])
895 ap = Workflow.OrFork(preAct, postActs, probs);
898 function ap = OrJoin(preActs, postAct)
899 % ORJOIN Create OR-join precedence
901 % AP = WORKFLOW.ORJOIN({PREACT1, PREACT2, ...}, POSTACT)
903 for a = 1:length(preActs)
904 if isa(preActs{a},
'WorkflowActivity')
905 preActs{a} = preActs{a}.name;
908 if isa(postAct,
'WorkflowActivity')
909 postAct = postAct.name;
911 ap = ActivityPrecedence.OrJoin(preActs, postAct);
914 function ap = Loop(preAct, postActs, counts)
915 % LOOP Create loop precedence
917 % AP = WORKFLOW.LOOP(PREACT, {LOOPACT, ENDACT}, COUNT)
918 % AP = WORKFLOW.LOOP(PREACT, LOOPACT, ENDACT, COUNT)
920 if isa(preAct,
'WorkflowActivity')
921 preAct = preAct.name;
924 for a = 1:length(postActs)
925 if isa(postActs{a},
'WorkflowActivity')
926 postActs{a} = postActs{a}.name;
929 elseif isa(postActs,
'WorkflowActivity')
930 postActs = postActs.name;
932 ap = ActivityPrecedence.Loop(preAct, postActs, counts);
935 function wf = fromWfCommons(jsonFile, options)
936 % FROMWFCOMMONS Load a workflow from a WfCommons JSON file.
938 % WF = WORKFLOW.FROMWFCOMMONS(JSONFILE)
939 % WF = WORKFLOW.FROMWFCOMMONS(JSONFILE, OPTIONS)
941 % Loads a workflow trace from the WfCommons format
943 % LINE Workflow
object for queueing analysis.
946 % jsonFile - Path to WfCommons JSON file
947 % options - Optional struct with:
948 % .distributionType - 'exp' (default), 'det', '
aph', 'hyperexp'
949 % .defaultSCV - Default SCV for APH/HyperExp (default: 1.0)
950 % .defaultRuntime - Default runtime when missing (default: 1.0)
951 % .useExecutionData - Use execution data if available (default: true)
952 % .storeMetadata - Store WfCommons metadata (default: true)
955 % wf - Workflow
object
958 % wf = Workflow.fromWfCommons('montage-workflow.json');
960 % fprintf('Mean execution time: %.2f\n', ph.getMean());
965 wf = WfCommonsLoader.load(jsonFile, options);