Flow diagrams to Petri nets

This document describes how the FD2PN component has been reused for several workflow languages. This component features a reusable transformation from a Workflow concept to a Petri net, which is able to deal with several of the patterns defined in the Workflow patterns catalog.

The menu provides links to the different artefacts of the case study.

Code for Bentō:
Template:
Concepts:
Meta-models:

Transformation template

The template transforms each task and exclusive-choice node into a place, each parallel-split and synchronization node into a transition, and each simple-merge node into a place connected to a transition. The rule to transform a multi-choice node is the most complex one, as it creates 2^n-1 Petri net transitions (with n being the number of output nodes of the multi-choice) connected to a different combination of the output nodes each (i.e. to one of the outputs, to two of them, and so on). This models the fact that a multi-choice node diverges the execution into several concurrent threads, but the selection of which ones is resolved at runtime. Additional rules connect places and transitions through arcs according to the defined flow edges, and create intermediate objects (e.g. a transition) if the elements to connect have the same type (e.g. two places). Finally, if the language contains terminating final tasks, the template adds a place Control connected to all transitions in the net as follows: with an arc if the transition is connected to a terminating final task, and with a loop if it is not connected to a terminating final task.

-- @path FD=/flow_diagrams/metamodels/flow_concept.ecore
-- @path PN=/flow_diagrams/metamodels/petri_nets.ecore

module flow2pn;
create OUT : PN from IN : FD;

helper context FD!Node def: typeName : String  = 
  self.oclType().name; 
helper context FD!Node def: isTask : Boolean = 
  self.oclIsKindOf(FD!Task);
helper context FD!Node def: isFinalTask : Boolean = 
  self.oclIsKindOf(FD!FinalTask);
helper context FD!Node def: isTerminatingTask : Boolean = 
  if self.isFinalTask then 
    self.isTerminating 
  else 
    false 
  endif;
helper context FD!Node def: isParallelSplit : Boolean = 
  self.oclIsKindOf(FD!ParallelSplit);
helper context FD!Node def: isSynchronization : Boolean = 
  self.oclIsKindOf(FD!Synchronization);
helper context FD!Node def: isExclusiveChoice : Boolean = 
  self.oclIsKindOf(FD!ExclusiveChoice);
helper context FD!Node def: isSimpleMerge : Boolean = 
  self.oclIsKindOf(FD!SimpleMerge);
helper context FD!Node def: isMultiChoice : Boolean = 
  self.oclIsKindOf(FD!MultiChoice);
helper context FD!Node def: toPlaceAsInput : Boolean = 
  self.isTask or self.isFinalTask or self.isExclusiveChoice or 
  self.isSimpleMerge or self.isMultiChoice;
helper context FD!Node def: toPlaceAsOutput : Boolean = 
  self.isTask or self.isFinalTask or self.isExclusiveChoice;
helper context FD!Node def: toTransitionAsInput : Boolean = 
  self.isSynchronization or self.isParallelSplit;
helper context FD!Node def: toTransitionAsOutput : Boolean = 
  self.isSynchronization or self.isParallelSplit or 
  self.isSimpleMerge or self.isMultiChoice;

 -- it returns n^m
helper context Integer def: pow (m:Integer) : Integer = 
  if (m>1) then 
    self*self.pow(m-1) 
  else 
    self 
  endif;

-- it returns Sequence{1, 2, ... n}
helper context Integer def: toSequence () : Sequence(Integer) = 
  if self<=0 then 
    Sequence{} 
  else 
    Sequence{self}.union((self-1).toSequence()) 
  endif; 

-- used by rule create_transition
helper context Integer def: choices () : Sequence(Sequence(Boolean)) = 
  if self>1 then 
    (self-1).choices()
    ->collect(n | n->append(true)) 
    ->union((self-1).choices()
    ->collect(n | n->append(false)))
  else
    Sequence{Sequence{true},Sequence{false}}
  endif;

helper context FD!MultiChoice def: nodeGenerator : 
  Sequence(TupleType(source : FD!Node, num : Integer)) =
  (2.pow(self.outs->size())-1).toSequence()
  ->iterate(n; result: Sequence(TupleType(source : FD!Node, num : Integer)) = Sequence {} |
    let tuple : 
      OclAny = Tuple { source : FD!Node = self, num : Integer = n } 
    in	
      result.including(tuple)		
  );

rule create_transition(source : FD!Node, num : Integer) {
  using { 
    choices : Sequence(Boolean) = 
      source.outs->size().choices()->at(num); 
    icontrol : Sequence(OclAny) 
      source.refImmediateComposite().control; 
    ocontrol : Sequence(OclAny) = 
      if source.isConnectedToTerminating then 
        Sequence{} 
      else 
        icontrol 
      endif; 
  }
  to  transition : PN!Transition (
    name <- source.typeName + ' ' + num,
    in   <- icontrol.append (source),
    out  <- source.outs->size().toSequence()
      ->iterate (i; result:Sequence(FD!Node)=Sequence{} | 
        if choices->at(i)=true then
          result->append( 
            if source.outs->at(i).out.toPlaceAsInput then
              source.outs.at(i).out
            else 
              source.outs.at(i)
            endif)
        else
          result
        endif) 
      .append( ocontrol )
  ) 
  do { 
    transition;
  }
}

-- if the diagram contains terminating final tasks, we add a place 
-- 'Control' connected to all transitions in the net as follows:
--   1) with an arc, if the transition is connected to a terminating final task
--   2) with a loop, if it is not connected to a terminating final task
helper context FD!FlowEdge def: isConnectedToTerminating : Boolean =
  self.out.isTerminatingTask;

helper context FD!Node def: isConnectedToTerminating : Boolean = 
  self.outs->exists(e | e.isConnectedToTerminating);

helper context FD!FlowDiagram def: control : OclAny = 
  if self.nodes->exists(n | n.isTerminatingTask) then 
    Sequence{ thisModule.get_control(self) }
  else 
    Sequence{} 
  endif;

unique lazy rule get_control {
  from flow : FD!FlowDiagram
  to  place : PN!Place (      
    name <- 'Control',
    tokens <- 1
  )
}
	
-- the flow diagram is transformed into a Petri net 
rule flowdiagram {
  from flow : FD!FlowDiagram 
  to   net  : PN!PetriNet (
    elems <- flow.nodes.append(
      flow.edges
     .append(flow.nodes->select(n | n.isSimpleMerge)
        ->collect(n | thisModule.create_transition(n, 1) ))).union( 
          flow.nodes->select(n | n.isMultiChoice)
          ->collect(n | n.nodeGenerator)->flatten()
          ->collect(n | thisModule.create_transition(n.source, n.num) ))
         .append(flow.control)	
	)
}

-- each task is transformed into a place, with 1 token if it is initial
rule task { 
  from task  : FD!Task 
  to   place : PN!Place (
    name   <- if task.isInitial then 'Initial' else task.name endif,
    tokens <- if task.isInitial then 1 else 0 endif
  )
}

-- each final task is transformed into a place
rule finaltask { 
  from task  : FD!FinalTask 
  to   place : PN!Place (
    name <- 'Final' + 
      (if task.isTerminating then 
        ' Terminating' 
      else 
        '' 
      endif),
    tokens <- 0
  )
}

-- each parallel split  (i.e. fork) is transformed into a transition
-- each synchronization (i.e. join) is transformed into a transition
rule synchronization_all {
  from synch : FD!Node (synch.isParallelSplit or synch.isSynchronization)
  using { 
    icontrol : Sequence(OclAny) = 
      synch.refImmediateComposite().control; 
    ocontrol : Sequence(OclAny) = 
      if synch.isConnectedToTerminating then 
        Sequence{} 
      else 
        icontrol 
      endif; 
  }
  to   transition  : PN!Transition (
    name <- synch.typeName,		
    in <- synch.ins->select(e | e.in.toPlaceAsOutput)
      ->collect(e | e.in) 
      ->union( synch.ins->select(e | e.in.toTransitionAsOutput) ) 
      .append( icontrol ),    				
    out  <- synch.outs->select(e | e.out.toPlaceAsInput)
      ->collect(e | e.out)  
      ->union( synch.outs->select(e | e.out.toTransitionAsInput) )
      .append( ocontrol )
  ) 		
}

-- each exclusive choice is transformed into a place
rule exclusive_choice {
  from choice : FD!ExclusiveChoice
  to   place  : PN!Place (
    name   <- choice.typeName,
    tokens <- 0
  )
}

-- each simple merge is transformed into a place connected to a 
-- transition; the transition is created by rule create_transition, 
-- invoked from rule flow_diagram
rule simple_merge {
  from merge : FD!SimpleMerge
  to 	 place : PN!Place (
    name <- merge.typeName,
    tokens <- 0
  )
}

-- each multi choice is transformed into a place connected to (2^n)-1 
-- transitions; the transitions are created by rule create_transition, 
-- invoked from rule flow_diagram
rule multi_choice {
  from choice : FD!MultiChoice
  to 	 place  : PN!Place (
    name <- choice.typeName,
    tokens <- 0
  )
}

-- flow edges connecting nodes that are transformed into places, 
-- are transformed into transitions
rule place_place {
  from edge : FD!FlowEdge 
    ( edge.in.toPlaceAsOutput and 
      edge.out.toPlaceAsInput )
  using { 
    icontrol : Sequence(OclAny) = 
      edge.refImmediateComposite().control; 
    ocontrol : Sequence(OclAny) = 
      if edge.isConnectedToTerminating then 
        Sequence{} 
      else 
        icontrol 
      endif; 
  }
  to transition  : PN!Transition (
    in  <- icontrol.append (edge.in),
    out <- ocontrol.append (edge.out)
  )
}

-- flow edges connecting nodes that are transformed into transitions, 
-- are transformed into places
rule transition_transition {
  from edge  : FD!FlowEdge 
    ( edge.in.toTransitionAsOutput and 
      edge.out.toTransitionAsInput ) 
  to   place : PN!Place (
    name   <- edge.in.typeName + '-' + edge.out.typeName,
    tokens <- 0
  )
}