The Mathematics of Flow
Learning objective: Read and write the formal notation; understand firing rules and state equations.
Chapter 1 introduced Petri nets informally — circles, bars, arrows, dots. That’s enough for intuition, but not enough for analysis. To prove that a system can’t deadlock, or that a resource pool never goes negative, or that a workflow always terminates, you need precise definitions. This chapter provides them.
The notation looks heavy at first. It isn’t. Everything here follows from two ideas: a net is a bipartite graph with weights, and a marking is a vector of integers. The rest is matrix arithmetic.
The Formal Definition
A Petri net is a 5-tuple:
Where:
- is a finite set of places
- is a finite set of transitions
- is the flow relation — the set of arcs
- is the arc weight function
- is the initial marking
Two constraints keep things clean: (nothing is both a place and a transition) and (the net isn’t empty).
That’s the entire definition. Five things. Places are where tokens live. Transitions are what moves them. Arcs say who connects to whom. Weights say how many tokens an arc carries. The initial marking says where tokens start.
Presets and Postsets
Every transition has inputs and outputs. The notation for these is compact and worth memorizing:
- — the preset of (input places)
- — the postset of (output places)
The same notation works for places:
- — transitions that feed into
- — transitions that feeds into
The bullet notation reads naturally: means “what comes before ,” and means “what comes after .”
Markings and the State Vector
A marking assigns a non-negative integer to each place — how many tokens it holds. Since a net has places, we can write the marking as a column vector:
This is the state vector. It captures everything about the system’s current state. Two markings are equal if and only if every place has the same token count in both. There’s no hidden state, no side channels, no context beyond the vector.
The initial marking is the starting state. For the traffic light from Chapter 1:
Every marking you can reach by firing transitions is a point in — non-negative integer coordinates, one per place. The set of all reachable markings, starting from , is the reachability set . For the traffic light, contains exactly three points: the token in Green, the token in Yellow, the token in Red.
Firing Rules and Reachability
A transition fires by consuming tokens from its input places and producing tokens in its output places. But it can only fire when it has enough tokens to consume. The enabling condition is:
Every input place must have at least as many tokens as the arc weight demands. If any input place is short, the transition is blocked.
When an enabled transition fires, the new marking is:
For every place : subtract the tokens consumed (arc from to ) and add the tokens produced (arc from to ). If isn’t connected to , both weights are zero and — unconnected places are unaffected.
A Worked Example
Consider a simple three-place chain:
[p1] -> t1 -> [p2] -> t2 -> [p3]
With initial marking (one token in ).
Step 1: Is enabled? Its only input is , and . Yes. Fire :
Step 2: Is enabled? Its only input is , and . Yes. Fire :
The token has moved from through to . The reachability set is . Three reachable markings, matching intuition — the token can be in exactly one of three places.
Firing Sequences
A firing sequence is an ordered list of transitions: . The sequence is valid from marking if each transition is enabled when it’s supposed to fire — is enabled at , is enabled at the marking after fires, and so on.
We write to mean “firing sequence is valid from and leads to .” The reachability set is then:
Nondeterminism
When multiple transitions are enabled simultaneously, either one could fire. The Petri net doesn’t prescribe which. This nondeterminism is a feature, not a bug — it models real concurrency where the order of independent events is genuinely unspecified.
In the traffic light, only one transition is ever enabled at a time, so there’s no nondeterminism. But in the intersection model from Chapter 1, when the shared token is available, both lights could claim it. The net structure constrains what’s possible; it doesn’t dictate the schedule.
The Incidence Matrix
Computing markings by hand gets tedious. The incidence matrix makes it mechanical.
The incidence matrix has one row per place and one column per transition. Each entry is the net effect of firing that transition on that place:
Positive means the transition produces tokens in that place. Negative means it consumes. Zero means no effect.
Building the Matrix
For the three-place chain:
Reading column : firing removes one token from and adds one to . Reading column : firing removes one from and adds one to . Each column is a change vector — the delta applied to the marking when that transition fires.
The State Equation
This is the payoff. With the incidence matrix, the firing rule becomes matrix multiplication:
Where is the firing count vector — how many times each transition fires. For a single firing of , :
Fire both transitions once () to go directly from to :
The state equation is powerful because it’s linear. Given any firing count vector, you can compute the resulting marking with one matrix multiplication. But there’s a caveat: the state equation doesn’t check enabling. A firing count vector might describe a sequence that isn’t valid — some transition might need tokens that aren’t there yet. The state equation gives you a necessary condition for reachability, not a sufficient one.
A Richer Example
Consider a net where a transition has two inputs — the synchronization pattern:
[p1] --> t1 --> [p3]
[p2] --/
Transition needs a token from both and to fire. The incidence matrix:
With , firing gives . With , is not enabled — has no token. The synchronization is enforced by the structure.
P-Invariants and Conservation Laws
The incidence matrix reveals something deeper than individual firings. It reveals what cannot change, no matter what sequence of transitions fires.
A P-invariant (place invariant) is a row vector such that:
The vector assigns a weight to each place such that the weighted sum of tokens is preserved by every transition. If is a P-invariant, then for any reachable marking :
This is a conservation law. No matter what fires, no matter what order, the weighted token count stays constant.
Finding P-Invariants
For the three-place chain, try :
It works. The invariant says:
The total number of tokens is always 1. We proved this for the traffic light informally in Chapter 1 — now we’ve proved it with linear algebra, and the proof works for any net where .
What Conservation Laws Tell You
P-invariants answer practical questions:
“Can tokens leak?” If is an invariant, the total token count is constant. The net is closed — nothing is created or destroyed. This is the Petri net equivalent of conservation of mass.
“Is this resource pool managed correctly?” If two places (available) and (busy) satisfy for some constant , then resources are always either available or busy, never duplicated or lost.
“Is the net bounded?” If every place appears in at least one P-invariant with a positive coefficient, then no place can accumulate unbounded tokens. The net is structurally bounded — it stays within limits regardless of the initial marking.
Partial Invariants
Not every net has a P-invariant covering all places. A net with a “source” transition (one that produces tokens from nothing) or a “sink” transition (one that consumes tokens into nothing) breaks total conservation. But subsets of places may still be conserved.
For example, in a producer-consumer system:
[source] -> t_produce -> [buffer] -> t_consume -> [sink]
The total token count isn’t conserved — creates tokens and destroys them. But if you add a capacity limit by looping tokens back:
[empty_slots: 5] -> t_produce -> [buffer] -> t_consume -> [empty_slots]
Now is an invariant. The buffer can never exceed 5.
In Code
The go-pflow library computes incidence matrices and checks invariants directly:
analyzer := reachability.NewInvariantAnalyzer(net)
matrix, places, transitions := analyzer.IncidenceMatrix()
invariants := analyzer.FindPInvariants(initialMarking)
for _, inv := range invariants {
fmt.Printf("Conserved: %v = %d\n", inv.Coefficients, inv.Value)
}
// Check if total tokens are conserved
isConservative := analyzer.CheckConservation(initialMarking)
The IncidenceMatrix method builds the matrix from the net’s arc structure. FindPInvariants solves to find conservation laws. CheckConservation tests whether the all-ones vector is an invariant — whether total token count is preserved.
Putting It Together
The mathematics of this chapter — the 5-tuple, the state equation, the incidence matrix, P-invariants — form a complete toolkit for structural analysis. Given a Petri net, you can:
- Write the incidence matrix from the arc structure
- Compute reachable markings using the state equation
- Find conservation laws by solving
- Prove boundedness from positive P-invariants
- Check for deadlocks by finding markings where no transition is enabled
All of this is static analysis — you don’t run the net, you reason about its structure. This is what distinguishes Petri nets from simulation frameworks: the topology itself is a proof.
But there’s a limit. Discrete Petri nets track individual tokens and individual firings. For large systems — thousands of tokens, complex topologies — the reachability set can be enormous. The next chapter shows how to break through this limit by letting tokens be real numbers and transitions fire continuously, turning the state equation into a differential equation.