The Michaelis-Menten equation says why an enzyme-catalyzed reaction saturates: past some substrate concentration, adding more substrate stops speeding it up. The textbook derivation starts from four rate equations, assumes the enzyme-substrate complex sits at a quasi-steady state, and does the algebra. A 4-place Petri net with mass-action rates gives you those four rate equations without anyone typing them, and the solver integrates them as they stand — no steady-state assumption. The Michaelis-Menten law is still an approximation of that system, and it still comes from the algebra; what the net changes is that the thing being approximated is the model itself, not a hand transcription of it.
Try the interactive demo at pilot.pflow.xyz/enzyme-kinetics. Its source is frontends/enzyme-kinetics/main.js in petri-pilot, which builds the net in buildModel; the same net with the same default rates is declared for the generated service in services/enzyme-kinetics.json. If the ODE view of a Petri net is new, Declarative Differential Models is the background.
An enzyme catalyzes the conversion of substrate to product in three steps:
E + S ⇌ ES → E + P
The enzyme is not consumed—it cycles between free and bound states.
Four places, three transitions:
| Place | Initial | Role |
|---|---|---|
substrate |
100 | Free substrate molecules |
enzyme |
10 | Free enzyme molecules |
complex |
0 | Enzyme-substrate complex |
product |
0 | Converted product |
| Transition | Rate | Arcs |
|---|---|---|
bind |
k1 = 0.01 | substrate + enzyme → complex |
unbind |
k-1 = 0.1 | complex → substrate + enzyme |
catalyze |
kcat = 0.5 | complex → product + enzyme |
The arc structure encodes the chemistry directly. bind consumes a free enzyme and both unbind and catalyze give one back — the enzyme is a catalyst because every path out of complex returns it.
With mass-action kinetics, each transition fires at a rate proportional to the product of its input concentrations:
flux(bind) = k1 × [S] × [E]
flux(unbind) = k-1 × [ES]
flux(catalyze) = kcat × [ES]
The ODE system follows directly from the Petri net topology:
d[S]/dt = -k1·[S]·[E] + k-1·[ES]
d[E]/dt = -k1·[S]·[E] + k-1·[ES] + kcat·[ES]
d[ES]/dt = k1·[S]·[E] - k-1·[ES] - kcat·[ES]
d[P]/dt = kcat·[ES]
We didn't write these equations into the simulation. The demo builds the net as JSON-LD and hands it to pflow-xyz's petri-solver.js, whose buildODEFunction derives each derivative from the arcs — flux is the rate times the product of the input markings. (The service definition does carry the four equations as metadata, for people to read; the solver never sees them.)
Set d[ES]/dt ≈ 0 — the quasi-steady-state assumption — use [E] + [ES] = [E]total, and the reaction rate simplifies to:
v = Vmax × [S] / (Km + [S])
where Km = (k-1 + kcat) / k1 and Vmax = kcat × [E]total. That step is algebra, and the demo doesn't do it numerically: its Km / Vmax readout is those two formulas applied to the slider values (computePredictions in main.js), not something fitted to the simulated curve. The approximation is good when [E]total is small against [S] + Km — at the defaults, 10 against 160 — and the net's ODE is what it approximates.
The demo re-integrates on every slider change (Tsit5, fixed step dt = 0.05, over a 30-second default window) and shows four readouts:
| Readout | Default Value | What It Is |
|---|---|---|
| Time to 50% conversion | ~17s | First time free substrate falls to half its initial value |
| Peak reaction rate | ~2.95 | Largest finite-difference d[P]/dt in the run |
| Final yield | ~70% | Product as a percentage of initial substrate at the end of the window |
| Km / Vmax | 60 / 5.0 | Computed from the rate constants, not measured from the run |
The first three come out of the integration; the figures are the demo's readout at the default sliders, and we haven't re-run them for this revision. There is one check you can do without running anything. d[P]/dt = kcat × [ES], and [ES] can never exceed the 10 enzyme molecules there are, so the peak is strictly bounded by Vmax = 5. The Michaelis-Menten approximation is tighter: at [S] = 100 it predicts 5 × 100 / 160 ≈ 3.1, and substrate only falls from there. A peak of ~2.95 sits just under that, which is what you'd expect when some substrate is already tied up in complex by the time the rate peaks.
The concentration chart shows the familiar pattern: substrate depletes as product accumulates, with the enzyme-substrate complex rising quickly then falling as substrate runs out.
The rate chart plots d[P]/dt against time, with a dashed line at Vmax. The page labels it a saturation curve, but it isn't one — the saturation curve is rate against substrate concentration — and its shape is different: the rate rises while the complex forms, peaks below Vmax, then falls as substrate runs out.
Why does the rate saturate? The Petri net makes it visible.
At low substrate, most enzyme is free. Increasing [S] means more binding events, so the rate climbs linearly. But at high substrate, almost all enzyme is tied up in complex. Adding more substrate can't speed things up—there's no free enzyme to bind with.
Km is the substrate concentration at half-maximum rate: a low Km means the enzyme is half-saturated at low substrate, a high Km means it needs a lot. It is often read as binding affinity, but that reading only holds when kcat ≪ k-1. At the defaults kcat is five times k-1, and Km = 60 against a dissociation constant k-1 / k1 = 10 — most of Km here is turnover, not loose binding.
In the Petri net, both effects are the competition for complex tokens. unbind and catalyze drain the same place, and either one empties it; when k-1 is large relative to kcat the complex mostly falls apart, when kcat is large it mostly turns over, and in both cases you need more substrate to keep the enzyme occupied.
The demo doesn't plot the hyperbola itself. To trace it you'd step the substrate slider and note the peak rate each time, which is roughly the initial rate at that [S] — a sweep we haven't automated (see the end of the post).
The sliders auto-simulate on every change. Some experiments to try:
High enzyme, low substrate (e0=50, s0=20): There's plenty of enzyme to go around, so the reaction runs much faster. This is also where the Km / Vmax readout stops meaning much. With more enzyme than substrate the quasi-steady-state assumption fails, and the Vmax of 25 it reports is unreachable: the complex can hold at most the 20 substrate molecules there are, so d[P]/dt never exceeds 0.5 × 20 = 10.
Low k1 (k1=0.001): Binding becomes the bottleneck. Km goes to (0.1 + 0.5) / 0.001 = 600, past the substrate slider's maximum of 500, so the demo can't put this enzyme at half-saturation at all.
High kcat (kcat=2.0): The enzyme turns over faster. Vmax goes to 20, and the 50% conversion time drops. But Km also rises, to 210 — faster catalysis empties the complex faster, so more substrate is needed to keep it populated.
Equal k-1 and kcat (km1=0.5, kcat=0.5): The complex is equally likely to unbind or catalyze. Km = (0.5 + 0.5) / 0.01 = 100—higher than default. The enzyme is less efficient because half the binding events are "wasted" on unbinding.
The Petri net enforces a conservation law automatically:
[E] + [ES] = [E]total = constant
Free enzyme plus bound enzyme always equals the initial enzyme count. Nothing in the demo enforces this; it follows from the arcs. The enzyme token circulates through enzyme → complex → enzyme but never leaves the system.
Similarly:
[S] + [ES] + [P] = [S]initial
Substrate is either free, bound in complex, or converted to product. Each transition leaves that sum unchanged: bind takes one substrate and makes one complex, unbind reverses it, catalyze turns one complex into one product.
These are P-invariants of the Petri net — weight vectors over the places whose weighted token count no firing can change. Over (substrate, enzyme, complex, product) they are w = (0, 1, 1, 0) and w = (1, 0, 1, 1), and each satisfies wᵀC = 0. We checked that by hand, transition by transition, against the nine arcs in the service definition. The coffee shop model gets its conservation laws the same way, one per ingredient (coffee_beans + beans_used = 1000).
The shape — a limited resource cycling between free and occupied, with throughput capped by how much of it there is — looks familiar from elsewhere: processes and cores, callers and agents, jobs and machines. We haven't built any of those as nets, and the mapping is looser than swapping labels. A call center has no obvious unbind (a caller abandoning mid-service, maybe), and a context switch is overhead on the core rather than a reversible binding. What should carry over is the enzyme invariant — free plus busy is constant — and the ceiling that comes with it: throughput can't exceed rate × servers, for the same reason d[P]/dt can't exceed kcat × [E]total.
View in pflow editor → · more ODE demos
The net is small enough to understand completely, and what it establishes is narrower than the earlier version of this post claimed. It generates the mass-action ODE system without anyone writing it, it carries two conservation laws in its arcs, and the demo's readout at the default setting is consistent with Michaelis-Menten (the peak-rate check above). It does not, by itself, produce the Michaelis-Menten equation; that is still the quasi-steady-state algebra above. The worked case that would close the gap is a sweep: integrate the net across a range of initial substrate, take the initial rate at each, and plot it against Vmax × [S] / (Km + [S]). It should match closely at e0 = 10 and drift as e0 approaches [S] + Km. Nothing in the demo or the repo runs that sweep yet, so that is a prediction, not a result.
This topic is covered in depth in Chapter 9: Enzyme Kinetics of Petri Nets as a Universal Abstraction.
For readers of the February version (revised 2026-10-04):
catalyze; it is consumed by bind and returned by unbind or catalyze.Related: Declarative Differential Models · Coffee Shop Model