Petri notation cheatsheet
The #petri notation gives you a compact way to write a net: each arc takes a line, so you can
edit it while it plays. For a guided introduction, see Petri nets.
The model and Kotlin DSL are in afield.simulation.petri.PetriNets.kt; the notation's parser is in
PetriNotation.kt. The Kotlin DSL can express everything the notation can,
and more, because its arcs can take a lambda.
#petri
!grid 1/16
kick : 1
kick -[ hit @1/16 : bd ]-> hats
hats -[ back @1/16 : sd ]-> kick
// starts a comment; a leading #petri line is the editor's marker and is otherwise ignored.
Directives
Settings for the whole net. One per line, anywhere in the source.
| Example | What it means |
|---|---|
!timing step |
Chooses the firing semantics: step, event or timed. |
!grid 1/16 |
Sets the tick to one sixteenth of a cycle. Durations and delays are in cycles, so changing playback speed changes their length in seconds. |
!conflict priority |
Chooses how competitors for one token are resolved: order, priority, round-robin or random. |
!seed 7 |
Seeds !conflict random, so the same net plays the same way twice. |
The three firing semantics and the four conflict policies:
| Value | What it means |
|---|---|
step |
Maximal step: every enabled, non-conflicting transition fires on each grid tick. The default. |
event |
One transition per tick, the first in conflict order. |
timed |
A transition holds its consumed tokens for its duration before the firing completes. |
order |
Resolves competition in declaration order, so moving a transition earlier can change the rhythm. |
priority |
Largest pri= first. The default. |
round-robin |
Rotates the ordering from step to step, so an early declaration cannot always win a shared token. |
random |
A seeded weighted shuffle; each transition’s w= sets its relative weight. |
Places
A place is created the first time anything mentions it, so you only declare one to give it a marking, a capacity or a colour.
| Example | What it means |
|---|---|
: kick : 1 |
Declares kick with one black token. A place mentioned only by an arc starts empty. |
2 hats : 2 |
Places two black tokens in hats. Bare whole numbers are token counts, rather than numeric token colours. |
^ dst : 0 ^4 |
Creates an empty place with capacity four. Firings must respect the destination’s capacity. |
"…" verse : "bd ~ sd ~" |
Stores one pattern-valued token containing a whole rhythm. Quoted patterns are not expanded into several tokens at declaration time. |
'…' label : 'lead' |
Stores one literal string colour. Single quotes distinguish a label from a double-quoted musical pattern. |
# notes : #60 |
Stores one token whose numeric colour is 60. Use # when the number is a value to transform, rather than a token count. |
, notes : #60, #64, #67 |
Stores three separately coloured tokens in one place. Commas separate tokens; the marking is three, not their numeric sum. |
Double quotes are the form to remember: a quoted string is a pattern, so one token can carry a whole rhythm, and a place can hold a section of a song rather than a single hit.
Arcs
| Example | What it means |
|---|---|
-[ pre -[ hit ]-> post |
Declares transition hit, consuming from pre and producing into post. Its name can be reused on another line to add more arcs. |
]-> pool -[ sink ]-> |
Closes the transition with no output place, making a drain. Conversely, -[ spawn ]-> pool is a source. |
+ kick + clock -[ hit ]-> hats |
Requires tokens from both input places in one firing. Use + on the output side to produce into several places. |
* 2*hats -[ roll ]-> fill |
Consumes two tokens from hats for each firing. Arc weights apply independently to each named place. |
~ ~armed -[ fill ]-> hats |
Enables fill only while armed is empty. The inhibitor tests absence and consumes nothing. |
& &clock -[ tick ]-> bar |
Requires a token in clock but leaves it there. A read arc lets several transitions observe the same condition. |
A transition may be named on several lines, which keeps one with several inputs readable.
Transition settings
Written after the name, inside the brackets, in any order.
| Example | What it means |
|---|---|
@ pre -[ hit @1/16 : bd ]-> post |
Gives the emitted event a duration of one sixteenth of a cycle. Under timed semantics, the transition also holds its tokens for this duration. |
delay= pre -[ hit delay=1/32 : bd ]-> post |
Shifts the audible event later by one thirty-second of a cycle. The net’s enabling and token movement stay on their original schedule. |
pri= pre -[ hit pri=2 : bd ]-> post |
Gives this transition priority two. Larger values win when the conflict policy is priority. |
w= pre -[ hit w=2 : bd ]-> post |
Assigns relative weight two in random conflict ordering. The weight matters only under !conflict random. |
: pre -[ hit : bd ]-> post |
Emits the sound token bd when the transition fires. A quoted string or #number can be emitted instead. |
=> pre -[ move => rev ]-> post |
Transforms consumed colours before producing output tokens. Here the first consumed pattern is reversed. |
Combinators
What => can name. Each takes the tokens a firing consumed and returns the colour of the token it
produces. There are no others; for anything else, use the Kotlin
DSL, where an output arc takes a lambda.
| Example | What it means |
|---|---|
carry pre -[ move => carry ]-> post |
Passes the first consumed colour through unchanged. This is the default when no combinator is written. |
black pre -[ move => black ]-> post |
Produces an uncoloured black token. Use it when the next place needs occupancy rather than the incoming payload. |
swap pre -[ move => swap ]-> post |
Selects the second consumed colour, falling back to the first when there is only one. Input ordering therefore matters. |
stack pre -[ move => stack ]-> post |
Layers all consumed patterns simultaneously. Their events share the output pattern’s cycle. |
cat pre -[ move => cat ]-> post |
Plays the consumed patterns one per cycle. This preserves each section’s full cycle length. |
fastcat pre -[ move => fastcat ]-> post |
Fits all consumed patterns side by side into one cycle. More inputs make each section occupy a smaller fraction of that cycle. |
rev pre -[ move => rev ]-> post |
Reverses the first consumed pattern. Use it to alternate a section with its backward version. |
palindrome pre -[ move => palindrome ]-> post |
Alternates the first consumed pattern with its reverse. The output carries both directions as a pattern. |
degrade pre -[ move => degrade ]-> post |
Drops half the first consumed pattern’s events probabilistically. This thins a section without changing the net’s token logic. |
fast2 pre -[ move => fast2 ]-> post |
Plays the first consumed pattern at double speed. Its material repeats twice over the original span. |
slow2 pre -[ move => slow2 ]-> post |
Plays the first consumed pattern at half speed. Its material stretches across twice the original span. |
sum pre -[ move => sum ]-> post |
Adds the numeric colours of the consumed tokens. Use numeric tokens such as #60 for arithmetic payloads. |
inc pre -[ move => inc ]-> post |
Adds one to the first consumed numeric colour. A loop through this transition can carry a counter. |
count pre -[ move => count ]-> post |
Produces the number of tokens consumed by this firing. This measures the firing’s inputs rather than the total marking of the net. |
Kotlin entry points
The Kotlin calls that build, play and analyse a net.
| call | gives you |
|---|---|
petri("…") |
Parses the notation into a PetriNet and throws when parsing fails. Use petriParse when you need to display diagnostics instead. |
petriParse("…") |
Returns collected diagnostics and a nullable parsed net. Check ok before treating the result as valid. |
petri { … } |
a PetriNet from the Kotlin DSL, where arcs can take lambdas |
petriRing(name, pulses, steps) |
a Euclidean token ring — the drum primitive |
petriSong("verse" to (pat("…") to 8), …) |
a linear form whose sections are pattern-valued tokens |
net.run() |
a memoised, deterministic trajectory |
net.live() |
a stateful instance you can inject, remove and force |
net.pattern() |
PatPattern<PetriFiring>, so every pattern operation applies |
net.emits() |
PatPattern<String>, ready for notes(), Dirt, or a synth |
pattern.unfold<String>() |
plays the pattern each firing consumed, across that firing's arc |
run.signal(place) |
the place's token count as a continuous control signal |
run.level(place, full) |
the same, normalised to 0..1 for range and rangex |
run.gate(place) |
1 while the place is occupied, 0 when it is not |
net.period() |
how long the net takes to come back round, in cycles |
net.tInvariants() |
its minimal loops, from the incidence matrix |
net.willDeadlock() |
whether it can get stuck with nothing able to fire |
net.deadTransitions() |
the voices that silently never fire |
net.report() |
all of the above, as a paragraph |
net.stateSpace() |
every marking reachable by any interleaving, for the checks below |
graph.deadlockTrace() |
the shortest firing sequence that gets it stuck, or null |
graph.witness { … } |
EF: the shortest route to a marking satisfying the predicate |
graph.invariant { … } |
AG: the shortest violation, or null when the property holds |
graph.mutuallyExclusive("a", "b") |
true / false / null — null means the search was truncated |
graph.bound(place), graph.isSafe, graph.isLive |
exact, or null when the budget ran out |
graph.neverFires() |
fireable in no interleaving — the strict deadTransitions() |
trace.verify() |
re-fires the counterexample through the engine; null means it holds up |
net.stateSpace(reduction = PetriReduction.STUBBORN) |
a much smaller graph for large nets; answers deadlock questions only |
graph.exact |
complete && reduction == NONE; everything but deadlock needs it |
graph.tooDenseToDraw() |
above PETRI_STATE_DRAW_LIMIT the drawing summarises instead of drawing |
graph.proves { … } |
true / false / null — the three-valued form of invariant |
net.coverability() |
Karp-Miller with ω — terminates even when the state space does not |
box.showPetriStateSpace(graph) |
the state graph, with the counterexample drawn over it |
box.showPetriStateSpace(graph, maxNodes = …) |
raise the node bound, or Int.MAX_VALUE to insist |
box.showPetriNet(live) |
the picture, clickable: click a place to inject, a transition to fire |
live.play(cps = 2.0) { … } |
plays a live net in a box, on the beat |
box.showPetriNet(net, layout = …) |
AUTO (the default), RING, FORCE, LAYERED, TALA |
PetriLayout.TALA |
d2's diagram engine, via the optional native/field-d2-layout; draws with FORCE when it is absent |