← Home
petri-net resource-modeling ode capacity

Coffee Shop Model

A coffee shop is the smallest resource model we have that still has a real question in it: five drinks draw on shared stocks, and we want to know which stock runs out first. The net here is the five-drink core of NewInventoryNet in go-pflow's examples/coffeeshop/inventory.go. That file also carries an iced latte, a sugar condiment and a refill transition per ingredient; we leave those out, the same cut Chapter 5 of the book makes. Recipes are arc weights, each ingredient gets a conservation law from a paired tracking place, and the rates give the depletion order. The interactive café on sim.pflow.xyz is a different net — see below.

Five drinks, shared stock

Drink Beans Water Milk Syrup Cups
Espresso 18g 30ml — — 1
Americano 18g 200ml — — 1
Latte 18g 30ml 180ml — 1
Cappuccino 18g 30ml 120ml — 1
Mocha 18g 30ml 150ml 2 pumps 1

Starting inventory: 1,000g beans, 10,000ml water, 5,000ml milk, 500 pumps of syrup, 100 cups. Orders arrive continuously, and the question is which stock goes first and how that moves when demand changes.

Places

The model has two categories of places. Ingredient places hold current stock:

Place Initial Role
coffee_beans 1000g Raw ingredient
water 10000ml Raw ingredient
milk 5000ml Raw ingredient
cups 100 Consumable
syrup 500 pumps Raw ingredient (mocha only)

Consumption tracking places count what's been used (all start at 0): beans_used, water_used, milk_used, cups_used, syrup_used. This paired structure creates conservation laws — every token consumed from an ingredient place appears in the corresponding tracking place.

Weighted Arcs as Recipes

Each drink requires specific quantities. The make_espresso transition has:

The arc weights are the recipe, and the conservation law follows from them: coffee_beans + beans_used = 1000 for all time. In go-pflow's example they are not the only copy of the recipe, though. inventory.go also keeps a Recipes map that CanMakeDrink reads, and the two drifted: Recipes listed an iced latte that the net had no transition for, so the shop offered a drink the net could not make, until make_iced_latte was added in August 2026 (the comment above it in inventory.go says why). The net can only be the specification if it is the one place the recipe lives.

Transition Rates

Each transition has a rate constant. InventoryRates gives them as drinks per minute at peak:

Transition Rate Drinks/hr
make_espresso 0.5 30
make_americano 0.3 18
make_latte 0.8 48 (most popular)
make_cappuccino 0.4 24
make_mocha 0.2 12

The continuous dynamics follow mass-action kinetics:

flux = rate × ∏[input places]

go-pflow's solver (solver/ode.go) uses the first power of each input's marking; the arc weight scales how much a firing consumes, not the exponent. As a stock depletes, every transition that reads it slows with it, with no scheduling logic. The product has a cost the drinks-per-minute column hides: a latte's flux carries a factor of milk (5,000) that an espresso's doesn't, so inside the ODE the rate constants are not drinks per minute at all, and the example multiplies every one of them by 0.0001 before solving (simulation.go). Scaling every constant by the same factor only changes the clock. The drinks-per-minute reading belongs to the linear estimate in the next section.

Which stock goes first

The order follows from how big a bite one drink takes out of each stock. A milk drink takes 120–180ml of 5,000ml milk (2.4–3.6%), 18g of 1,000g beans (1.8%), one of 100 cups (1%) and 30ml of 10,000ml water (0.3%). Reading the rate constants as drinks per minute — what PredictRunout in simulation.go does — gives a linear runout per stock, which can be checked by hand:

Stock Draw per minute Linear runout
milk 180(0.8) + 120(0.4) + 150(0.2) = 222 ml ≈ 22 min
coffee_beans 18 × 2.2 = 39.6 g ≈ 25 min
cups 2.2 ≈ 45 min
water 30(0.5 + 0.8 + 0.4 + 0.2) + 200(0.3) = 117 ml ≈ 85 min
syrup 2 × 0.2 = 0.4 pumps ≈ 1,250 min

Milk goes first, and once it runs low all three milk drinks stall, because milk is a factor in each of their fluxes. Beans go next, still drawn by espresso and americano. Cups are the smallest number in the shop and never come close: a latte takes 180 tokens of milk and one cup, and the weights decide the order, not the counts.

What we have not committed is an ODE run that shows this order with its own clock. Under product kinetics no stock reaches exactly zero — each decays, and the transitions that read it slow down with it — so "first" means "falls fastest", and the minutes above are the linear estimate's, not the ODE's. The conservation laws do hold along any ODE trajectory, because the solver adds 18v to beans_used for every 18v it takes from coffee_beans; checking coffee_beans + beans_used = 1000 in the solver output is a quick test of the wiring, though no test in the example asserts it yet.

Bottleneck Analysis

By adjusting rates, we can model different days. The minutes are linear estimates from the table above:

Scenario Change First Bottleneck
Slow day Halve all rates Milk still first, later (~45 min; beans ~50) — 5,000ml still doesn't last a shift
Normal Baseline rates Milk (~22 min)
Rush hour Double all rates Milk in half the time (~11 min)
Latte promotion Triple latte rate Milk still first and sooner (~10 min, 510ml/min); beans move up from ~25 to ~15 min

Each scenario is the same net with different rate constants. The structure — places, arcs, weights — stays the same. Only the dynamics change.

Model Structure

The full Petri net has ingredient places, tracking places, and one transition per drink type:

Places:
  coffee_beans, water, milk,         — Ingredients
  syrup, cups
  beans_used, water_used,            — Consumption tracking
  milk_used, syrup_used, cups_used

Transitions:
  make_espresso, make_americano,     — Drink preparation
  make_latte, make_cappuccino,
  make_mocha

Every arc runs from an ingredient place into a drink transition or from a drink transition out to a tracking place, with the same weight on both sides, so each ingredient and its tracking place sum to a constant — one conservation law per ingredient. Add the refill transitions from inventory.go and that pair no longer balances; the law widens to three places (stock, used and supply), which Chapter 5 works through.

Changing the rates moves the timings: milk leads at baseline, and a latte promotion makes it lead sooner while pulling beans up behind it. None of the four scenarios moves the order of the first two, and we haven't searched for a rate mix that would. What the rates can't change is which recipe touches which stock; that is fixed by the arcs.

The sim demo is a different net

The café on sim.pflow.xyz has three drinks fused from three separately written nets — counter, pantry and staff (16 places, 18 transitions) — with two baristas, capped stocks with restock transitions, customers who walk out, and no water and no tracking places. Its recipes are its own: espresso takes 20g of beans, a latte 15g and 50ml of milk, a cappuccino 15g and 30ml, against 2000g of beans, 2000ml of milk and 200 cups. The two diagrams below are from an older three-drink net again — an order → make → serve workflow (8 places, 12 transitions, services/coffeeshop.json) with the same espresso and latte recipes against 500ml of milk — not from the one above. None of these numbers will match the tables here. The two models haven't been reconciled; Small Models > LLMs notes the same gap.

Model Structure

Resource Consumption

What Changed

  1. 2026-08-22 — milk, not cups. This post originally said cups deplete first. They are the smallest stock, but the arc weights — 180 tokens of milk per latte against one cup — decide the order.
  2. 2026-10-11 — evidence and scope. The net is now named as the five-drink core of go-pflow's inventory.go, with links to it, simulation.go and the solver. Mocha's two pumps of syrup, missing from the recipe table, are back. The "slow day" row said nothing depletes during a shift; at half rates the linear estimate still runs milk out in about 45 minutes, which is what the row now says. The ODE figure "half the milk gone in roughly 21 minutes" came from a run that wasn't kept, and the example's ODE uses rescaled constants on a different clock, so it is gone; every minute figure is now labelled as the linear estimate. The rate constants are no longer called drinks per minute inside the ODE. "No separate configuration needed" is qualified: go-pflow's example kept a second copy of the recipes, and it drifted. The demo link and both diagrams are now labelled as the demo's three-drink net, and a hand-drawn depletion chart that showed beans going first (at 20g per espresso, from the demo net) was removed.
  3. 2026-10-23 — the demo moved. The interactive café is now sim.pflow.xyz's, a staffed 16-place net that is not the one the diagrams were drawn from; the section above says which is which.

Related: Declarative Differential Models · Skip the Spreadsheet: What-If Analysis · Small Models > LLMs · Enzyme Kinetics · Chapter 5: Resource Modeling of Petri Nets as a Universal Abstraction

×

Follow on Mastodon