The Azimuth Project
Petri net programming (Rev #14)

Contents

Introduction

This is the expository computing page for the article on Petri nets. These pages should be addressed to a range of computationally oriented readers, which includes coding enthusiasts, programmers, software engineers, computer scientists, and scientists who write code. The aim is first to provide a tutorial, then to discuss programming approaches, and then to present small programs that highlight the concepts.

Here are some guidelines:

  • The readers should be able to run these programs with a minimum of fuss. A scripting language that can represent abstractions is recommended.

  • Try keep the code as succinct and transparent as possible, while still aiming for code that runs.

  • Don’t increase usability at the expense of clarity.

  • Don’t improve performance at the expense of clarity. If there is a significant optimization that deserves to be discussed, consider presenting two versions of the program, starting with the one which is simpler but less efficient.

For an illustration, consider this section of code from the present article, which constructs a Petri net for a simplistic two-transition chemical reaction network, and then simulates it:

# combine: 2 H + 1 O --> 1 H2O
# split: 1 H20 --> 2 H + 1 O

petriNet = PetriNet(
    ["H", "O", "H2O"],    # states
    ["combine", "split"], # transitions
    [("combine",2,"H"), ("combine",1,"O"), ("split",1,"H2O")], # inputs
    [("combine",1,"H2O"), ("split",2,"H"), ("split",1,"O")],   # outputs
)

initialLabelling = {"H":5, "O":3, "H2O":4}

steps = 20
petriNet.RunSimulation(steps, initialLabelling)

Usability would be improved by passing the arguments from the environment. But that would swell the code with argument-parsing logic, which is routine and tangential to the subject at hand. It would also make the code less readable:

net = PetriNet(
    ParseStateArguments(argv[1]),
    ParseTransitionNames(argv[2]),
    ...

Definition of Petri nets

A Petri net is a kind of diagram involving two types of nodes: passive container nodes, called states (or places), which can hold some non-negative number of tokens, and active process nodes, called transitions. Each transition is wired to a fixed collection of containers called its inputs, and a fixed collection of containers called its outputs. The input collection for a transition may contain multiple instances of the same container, and similarly for the output collection. When the transition fires, it removes one token from each of its input containers, and adds one token to each of its output containers. When a transition contains multiple copies of the same container in its input collection, then the firing of the transition removes that many tokens from the input container. And if it has multiple outputs to the same container, then than many tokens are added to the container when fires. A transition is enabled if there are a sufficient number of tokens at its input containers. Dataflow can arise whenever one transition outputs to a container that is the input of another transition. The condition of the net as a whole is described by a labelling function that maps each container to the number of tokens that it holds.

The process structure of a Petri net is non-deterministic. In any given labelling, multiple transitions may be enabled, and so there are multiple ways to construct an execution sequence. If no transitions are enabled, then we say that the net is halted.

Petri nets are a good model for general reaction networks that consist of a collection of entities of various types, along with “reactions” that perform conversions between the types. Each object is represented by a token, and each container node holds all the objects for a certain type. A reaction transforms some input objects into some output objects, and this is reflected by the transition that removes tokens from its input containers and puts new tokens into its output containers.

The object containers are called states because each object type can be regarded as a different “state of being.”

Chemical reaction networks are a prime example: tokens are molecule instances, the containers correspond to molecular types, and the transitions are steps of a chemical reaction. Consider the H2O formation example from the main article. There are three states, H, O, and H2O, and one transition T, for the process of forming one H2O molecule from two H’s and one O. H and O are the inputs for T, with H occuring twice. H2O is the output for T. Naturally, when T fires, two tokens will be removed from H, one token will be removed from O, and one token will be added to H2O.

Application 1: Simulating a Petri net

We start with classes to simulate a Petri net. The rule for sequencing the transitions will be to randomly choose between the enabled transitions.

Running the program

The application will be for a chemical reaction network with two transitions: simplistic formation of water molecules, and reverse transition, simplistic dissociation of water molecules.

Here is the top-level call in the main program:

petriNet = PetriNet(
    ["H", "O", "H2O"],    # states
    ["combine", "split"], # transitions
    [("combine",2,"H"), ("combine",1,"O"), ("split",1,"H2O")], # inputs
    [("combine",1,"H2O"), ("split",2,"H"), ("split",1,"O")],   # outputs
    #
    # combine has 2 H inputs, 1 O input, and 1 H2O output 
    # split has 1 H20 input, 2 H outputs, and 1 O output 
)
initialLabelling = {"H":5, "O":3, "H2O":4}
steps = 10
petriNet.RunSimulation(steps, initialLabelling)

This produced the output

H, O, H2O, Transition
5, 3, 4, split
7, 4, 3, split
9, 5, 2, combine
7, 4, 3, combine
5, 3, 4, split
7, 4, 3, split
9, 5, 2, split
11, 6, 1, combine
9, 5, 2, split
11, 6, 1, split
13, 7, 0, done

Running it again gives a different sequence of transitions.

The code is HERE. This is a self-contained Python script, which can be run from the command prompt. Edit the first line of the program to point to the location of the interpreter.

Design of the program

We use object classes PetriNet and Transition. We can get by with a string to represent a state, by its name. (In a pedagogical program, less is more.)

The PetriNet class contains the list of state names, the list of transition names, the current labelling, which is a map from state names to integers, and a map from transition names to Transition objects.

The Transition class contains a map that describes the input connections. It maps each state name to the number of times that state is input to the transition. The transition class contains a similar map to describe the output connections.

The top-level method in PetriNet is RunSimulation, which makes repeated calls to a method called FireOneRule. FireOneRule constructs the list of enabled transitions, chooses one randomly, and fires it. This is facilitated by the methods IsEnabled and Fire on the Transition class.

Code for the program

 #!/usr/bin/python
#
# for Windows without cygwin, use this form for the top line:
#!C:\Python27\python.exe

import string
import random

# states are represented by their names 

class Transition:
    # Fields used in this class: 
    #
    # name -- transitionName
    # inputs: stateName -> inputCount
    # outputs: stateName -> outputCount 

    def __init__(this, transitionName):
       this.transitionName = transitionName
       this.inputs = {} 
       this.outputs = {} 

    def IsEnabled(this, labelling):
       for inputState in this.inputs.keys():
          if labelling[inputState] < this.inputs[inputState]: 
              return False  # not enough tokens to fire

       return True # good to go 

    def Fire(this, labelling):

       for inputName in this.inputs.keys():
          labelling[inputName] = labelling[inputName] - this.inputs[inputName] 

       for outputName in this.outputs.keys():
          labelling[outputName] = labelling[outputName] + this.outputs[outputName] 

class PetriNet:
    # Fields used in this class:
    #
    # transitionNames 
    # stateNames
    # transitions: transitionName -> TransitionObject
    # labelling -- mapping (dict) from state name to count 

    def __init__(this, stateNames, transitionNames, inputMap, outputMap):
        this.stateNames = stateNames
        this.transitionNames = transitionNames 

        this.BuildTransitions(inputMap, outputMap)

    def FireOneRule(this):

        enabledTransitions = filter(
            lambda transition: transition.IsEnabled(this.labelling),
            this.transitions.values())

        if len(enabledTransitions) == 0:

            # nothing can fire, we are halted
            return False 

        # pick a random enabled transition

        index = random.randrange(len(enabledTransitions))

        transition = enabledTransitions[index] 

        print transition.transitionName  

        transition.Fire(this.labelling)

        return True 

    def RunSimulation(this, iterations, initialLabelling): 

        this.PrintHeader()

        this.labelling = initialLabelling

        this.PrintLabelling() 
        
        for i in range(iterations):
           if this.FireOneRule():
               this.PrintLabelling(); 
           else:
               print "halt"
               break

        print "done"

    def BuildTransitions(this, inputSpecs, outputSpecs):

        this.transitions = {}

        for (transitionName, degree, stateName) in inputSpecs:
           this.GetTransition(transitionName).inputs[stateName] = degree 

        for (transitionName, degree, stateName) in outputSpecs:
           this.GetTransition(transitionName).outputs[stateName] = degree 
           
    def GetTransition(this, transitionName):
        if not(this.transitions.has_key(transitionName)):
            this.transitions[transitionName] = Transition(transitionName)

        return this.transitions[transitionName]

    def PrintHeader(this):
        print string.join(this.stateNames, ", ") + ", Transition"
           
    def PrintLabelling(this):
        for stateName in this.stateNames:
            print str(this.labelling[stateName]) + ",", 

# end class PetriNet

# now build a net for two opposite transitions: 
# combine: formation of water molecule
# split: dissociation of water molecule 

net = PetriNet(
    ["H", "O", "H2O"],    # states
    ["combine", "split"], # transitions
    [("combine",2,"H"), ("combine",1,"O"), ("split",1,"H2O")], # inputs
    [("combine",1,"H2O"), ("split",2,"H"), ("split",1,"O")],   # outputs
    #
    # combine has 2 H inputs, 1 O input, and 1 H2O output 
    # split has 1 H20 input, 2 H outputs, and 1 O output 
)

# and run it:

initialLabelling = {"H": 5, "O": 3, "H2O": 4}
steps = 10 

net.RunSimulation(steps, initialLabelling)

Stochastic Petri nets

A nice elaboration of the basic model is the stochastic Petri net, which consists of a Petri net, along with data that gives a rate coefficient for each transition. The firing rate for a transition will equal its rate coefficient times the product of the number of tokens at each input state (using multiple factors if a state occurs multiple times as an input).

This definition is motivated by the model of chemical reaction networks where the reaction rates are proportional to the product of the concentrations of the input constituents. We can think of the rate coefficient for a transition as a magnitude that takes into account both the “temperature” of the system of tokens and the “ease” with which the input tokens will combine to trigger the reaction, once they are brought into proximity.