← Home
petri-net what-if-analysis llm capacity simulation

Skip the Spreadsheet: What-If Analysis with Petri Nets

Every business plan has a spreadsheet: revenue projections, cost estimates, break-even calculations. They look precise, and they're mostly fiction, because a spreadsheet can't model flow. Customers arrive, wait, get served, and leave. Staff clock in and out. Rooms fill up and empty. A spreadsheet gives you totals; a simulation gives you bottlenecks.

The Experiment

We gave an LLM a simple prompt: "Model a veterinary clinic's daily operations." No formulas, no programming — just a plain English description of how the clinic works. Patients arrive, get triaged, see a vet or a technician, maybe need surgery or X-rays, then go home.

The LLM produced a Petri net — a kind of flowchart where tokens represent real things (patients, staff, rooms) and the rules enforce real constraints (you can't start surgery without a surgeon and a free operating room and a patient).

Then we hit simulate.

What Came Out

The clinic has been rebuilt since the first version of this post. The current model, services/vet-clinic.json, is 38 places and 42 transitions: two vets, three techs, one receptionist, four exam rooms, a surgery suite, a dental suite, one radiology room, six recovery kennels, and patients arriving at 6 an hour by default. Every service is two firings — start_X seizes the staff and the room, finish_X releases them — so how busy the vets are is a marking you can read rather than a number someone typed in. Walk-in clients who wait too long (about half an hour) give up and leave through walked_out, which is what makes "how many vets?" a question with an answer.

What the model is held to is six fitness gates in vetclinic_fitness_test.go. Each runs the shipped model unmodified — an 8-hour day, 30 stochastic realizations, a fixed seed — and asserts a direction rather than a number, so a rate tweak moves what the gates expect instead of breaking them. Patients are conserved: everyone who arrived is discharged, walked out, diverted, or still inside at close. Staffing has a knee: cutting to one vet raises walkouts, and going from two vets to four buys at most half of what the second vet bought. The clinic as sized is staff-limited, not room-limited — no room pool outranks every staff pool in the contention ranking. Closing the surgery-day gate (a read arc, so a scenario can flip it) stops procedures without stopping wellness exams. An emergency wave raises routine walkouts and diverts emergencies, and adding a vet and a tech buys some of that back. Those are the tests in the repo; I haven't re-run them for this revision. A later pass that ranks every knob by its measured effect (The Capacity Paradox) found the same shape from the other side: 8 of the clinic's 11 specialty rooms and pieces of equipment are inert, and front-of-house staffing — vets, techs, reception — is what binds.

That contradicts two things the first version said. It claimed a third vet helps only if there are also enough exam rooms, and that the receptionist is never the bottleneck; the gates and the diagnose pass point the other way on both. It also gave figures — two vets comfortable at 8 patients an hour, queues building at 12 and exploding at 15, the X-ray room idle 70% of the time, a surgery day costing 4 walk-in appointments — that have no test or recorded run behind them, from a model that no longer exists in that form, so they're withdrawn here rather than restated. The model does still carry the surgery-day mechanism that version described: recovery kennels are seized when a procedure finishes, so a full recovery ward backs up into the surgery suite. No gate measures how often that happens at the default rates.

The page also has a financial dashboard — revenue, costs, margin — computed in the browser from the ODE solution. That one runs on the Simulation tab, which still uses the older 33-place in-browser model rather than the one the What-If tab and the tests use; petri-pilot's TODO says so plainly: "the two tabs describe different clinics." Until that's retired, read the P&L and the what-if results as two models of a similar clinic, not one.

Try it yourself: pilot.pflow.xyz/vet-clinic

A Simpler Example: The Coffee Shop

Not every model needs 42 transitions. A coffee shop fits on a napkin: five ingredients (beans, water, milk, syrup, cups), five drinks (espresso, americano, latte, cappuccino, mocha), and a recipe per drink written as arc weights — an espresso is 18g of beans, 30ml of water and one cup.

Cups are the smallest stock, and the first version of this post said they run out first. They don't. Re-running the model with its own constants (Coffee Shop Model) says milk goes first, at roughly 22 minutes on a linear estimate, because three drinks each draw 120–180ml of milk against a single cup. Double the rush and milk goes in about half the time; run a latte promotion and it goes sooner still (~10 minutes), with beans moving up behind it. That reversal is the argument for simulating rather than eyeballing — the stock that looked like the bottleneck in a column of numbers wasn't, and the arc weights said so.

Try it: pilot.pflow.xyz/coffeeshop. The Go version is go-pflow/examples/coffeeshop.

Iterating in Conversation

Petri nets are intuitive enough for an LLM to generate and formal enough to simulate precisely. "Patients arrive, wait for a room, see a vet" maps directly to places and transitions, so you can iterate conversationally:

"What if we added a second surgery room?" "What happens during flu season when arrivals double?" "Show me the day if we cut one technician."

Each change adjusts the model, and each run gives you the consequence of the rules you described at the rates you gave them. That's exactly as trustworthy as those rates: the vet clinic's service times are declared in the model (~20 minutes for a wellness exam, ~60 for a spay), not measured from a real clinic.

Spreadsheets vs. Simulations

Spreadsheet Simulation
Models Totals and averages Flow and constraints
Shows End-of-day numbers How the day unfolds
Reveals Cost structure Bottlenecks
Answers "How much?" "What if?"
Updates Manually Change one parameter, re-run

A spreadsheet tells you what you'll make if you see 40 patients. A simulation can tell you whether two vets and four exam rooms can see 40 patients at all, and which pool runs out first when they can't.

Both demos are live: the vet clinic lets you change staffing, arrival rate, patience and day length and inject disruptions; the coffee shop shows which ingredient runs out first. The models are built with pflow, an open-source Petri net toolkit, and the code is in petri-pilot and go-pflow. The same what-if engine now backs sim.pflow.xyz (The Model Is the App), and The Capacity Paradox points it at a software team's own backlog instead of a shop floor.

What Changed

For readers of the March version (revised 2026-10-07):

  1. Coffee shop. "Cups run out first" was wrong; milk does, by a wide margin. The scenario claims now match the corrected coffee shop post.
  2. Vet-clinic numbers withdrawn. The 8/12/15 patients-per-hour thresholds, the 70%-idle X-ray room, the 4 lost walk-ins and the 33-patient ceiling had no test or recorded run behind them and came from a model since rebuilt. Replaced with what the fitness gates check.
  3. Two claims reversed. Exam rooms are not what limits a third vet, and reception is among the staff pools that bind, per the gates and the diagnose pass.
  4. "32 moving parts" described the old 32-transition model; the current one is 38 places and 42 transitions. The page's financial dashboard still runs on the old model, and the post now says so.

Related: Coffee Shop Model · The Model Is the App · The Capacity Paradox · Declarative Differential Models · Small Models > LLMs · Introducing Petri-Pilot

×

Follow on Mastodon