Inspection desks
Two desks measure what an etch line produces. Work arrives unevenly, the queue in front of the desks grows, and a lot that has waited half an hour is written off rather than measured. The question is whether to staff a third desk that stands itself down when the queue is short.
Every other example here asks which plant is better?. This one asks is the better plant worth it? — and the two questions give opposite answers.
Any unfamiliar word is in the glossary.
Why this example exists
A third desk makes the plant unambiguously better to stand in: time in the plant falls by a quarter, the wait at the desks by half, and almost nothing is written off any more. It also loses money, and the model says so with an interval that does not come near zero.
Neither number is wrong. They are answers to different questions, and a page that only reported
the flow would have recommended buying a desk that does not pay for itself. This is the whole
reason charges exists.
What a lot does
The desks are the constraint: two of them at ~240 s each is 120 s of desk time per lot against a lot every 150 s, so they are busy about 80% of the time — and at 80% a queue is already long enough that the half-hour patience bites.
The two models
The baseline, complete:
# yaml-language-server: $schema=https://clockwright.dev/schema/model.json
schema_version: 1
name: inspection-desks
tick_scale_per_second: 1000
horizon_seconds: 604800.0
warmup_seconds: 7200.0
resources:
- id: etch
servers: 2
- id: inspect
servers: 2
products:
- id: wafer
steps:
- resource: etch
name: etch
process_seconds:
kind: exponential
mean: 100.0
- resource: inspect
name: measure
process_seconds:
kind: exponential
mean: 240.0
patience_seconds:
kind: constant
value: 1800.0
releases:
- product: wafer
interarrival_seconds:
kind: exponential
mean: 150.0
charges:
- what: resources.inspect.state.Processing
rate: 0.02
label: desk time, working
- what: resources.inspect.state.Idle
rate: 0.02
label: desk time, waiting
- what: entities.completed
each: -9.0
label: revenue
- what: entities.abandoned
each: 4.0
label: lost sale
The candidate is that document with one block added to the inspect resource and nothing else
changed:
- id: inspect
servers: 2
capacity_policy:
up_to: 3
down_to: 2
add_above_queue: 3.0
remove_below_queue: 1.0
ready_after_seconds:
kind: exponential
mean: 600.0
min_engaged_seconds: 3600.0
Read it as a standing order rather than a schedule: while more than three lots are waiting, bring a third desk up; while fewer than one is waiting, stand it down again. The desk takes about ten minutes to arrive once called, and once it is up it stays up for at least an hour.
It is tempting to say it is, and measurement says otherwise: deleting min_engaged_seconds
moves the call-up count by 4% — 120 to 125 over ten replications, with the ranges overlapping —
and makes the money better by 215. The damping is done by the gap between the two
thresholds, and by the fact that a desk is only ever stood down when it is not in the middle
of a job. The floor does bind, just much higher up: at 100,000 s the desk comes up 15 times
instead of 120.
What comes out
{
"released": 4021,
"completed": 4002,
"abandoned": 24,
"cycle_seconds_mean": 701.5,
"resources": {
"inspect": {
"servers": 2,
"utilisation_pct": 80.24,
"mean_queue": 2.36,
"mean_wait_seconds": 345.5,
"capacity_engagements": 0,
"state_pct": { "Processing": 80.24, "Idle": 19.76 }
}
},
"money": {
"lines": [
{ "label": "desk time, working", "amount": 19409.98 },
{ "label": "desk time, waiting", "amount": 4781.32 },
{ "label": "revenue", "amount": -36018.0 },
{ "label": "lost sale", "amount": 96.0 }
],
"total": -11730.7,
"window_seconds": 604782.49
}
}
{
"released": 4021,
"completed": 4025,
"abandoned": 0,
"cycle_seconds_mean": 513.6,
"resources": {
"inspect": {
"servers": 3,
"utilisation_pct": 75.6,
"mean_queue": 1.05,
"mean_wait_seconds": 158.1,
"capacity_engagements": 125,
"state_pct": { "Processing": 53.71, "Idle": 17.36, "NonScheduled": 28.93 }
}
},
"money": {
"lines": [
{ "label": "desk time, working", "amount": 19491.32 },
{ "label": "desk time, waiting", "amount": 6299.03 },
{ "label": "revenue", "amount": -36225.0 },
{ "label": "lost sale", "amount": 0.0 }
],
"total": -10434.66,
"window_seconds": 604792.58
}
}
Those are one run each. To compare two models you need cwright compare, which
repeats both on the same random draws and reports a range.
cwright compare inspection-desks.yaml inspection-desks-policy.yaml \
--kpi cycle_seconds --replications 10 --indifference 1.0
Ten replications, on the development laptop (AMD Ryzen 7 7840U):
| KPI | verdict | two desks | third on call | difference |
|---|---|---|---|---|
cycle_seconds | Better | 700.31 | 518.79 | −181.52 [−195.25, −167.78] |
abandoned | Better | 29.50 | 0.30 | −29.20 [−39.24, −19.16] |
completed † | Better | 4019.40 | 4046.60 | +27.20 [+18.32, +36.08] |
money | ⚠️ Worse | −11865.66 | −10599.73 | +1265.93 [+1180.18, +1351.68] |
† completed is the one row that needs --higher-is-better. compare assumes lower is better,
which is right for a wait, a write-off and a net cost and wrong for output — and without the flag
it reports 27 more lots a week as Worse.
What it says
The plant gets better on every operational measure and the money gets worse by 1,266 a week, with an interval nearly fifteen half-widths clear of zero. Both readings are correct.
Where the money goes
The four lines say it exactly, and two of them carry it:
| two desks | third on call | change | |
|---|---|---|---|
| desk time, working | 19,409.98 | 19,491.32 | +81.34 |
| ⚠️ desk time, waiting | 4,781.32 | 6,299.03 | ⚠️ +1,517.71 |
| revenue | −36,018.00 | −36,225.00 | −207.00 |
| ⭐ lost sale | 96.00 | 0.00 | ⭐ −96.00 |
| total | −11,730.70 | −10,434.66 | +1,296.04 |
The policy spends 1,518 on idle desk to save 96 of write-offs. Working time barely moves, because the same lots need the same measuring however many desks are open — which is the conservation check that says the comparison is sound.
Check it on the numbers above: 19,409.98 / 0.02 is 970,499 desk-seconds, which is
80.2354% × 2 × 604,782.49 — the window_seconds the money block reports, which starts at the
first event after the warm-up, 17.5 s past it — and −36,018.00 / −9 is 4,002, exactly the
completed the document reports for the same week. The two arms' windows differ by 10 s,
because their first events after the warm-up do. ⚠️ Until #150 the rate lines were
priced over the whole run, warm-up included, and this page said "1,285 a week" where the
measured week reads 1,266.
Why idling costs more when it is cheaper
utilisation_pct falls, from 80.2% to 75.6%, on the plant that is working better. It is not
a contradiction: utilisation is busy time over available time, and a stood-down desk is not
available.
Multiply the state shares by the desks they are shares of:
| busy desks | idle desks | available desks | |
|---|---|---|---|
| two desks | 0.802 × 2 = 1.605 | 0.198 × 2 = 0.395 | 2.000 |
| third on call | 0.537 × 3 = 1.611 | 0.174 × 3 = 0.521 | 2.132 |
The same 1.61 desks' worth of work, spread over 2.13 desks of availability instead of 2.00 — and idle desk-seconds rise by a factor of 1.3174, which is the whole of the extra cost.
An Idle charge is rate × Idle% × servers × elapsed, and the elapsed time is identical in both
arms — so the two ratios are the same expression, and they would agree however the engine
behaved. It confirms that the table has been read correctly. It cannot confirm the engine, and an
identity that holds by construction never can.
The third desk is stood down for 28.93% of all station-time, which is 0.87 of a desk switched
off at any moment — and that is the whole point of the policy. The same plant with servers: 3
and no policy at all, ten replications:
| time in the plant | money against the baseline | |
|---|---|---|
| two desks | 700.31 s | — |
| ⭐ a third on call | 518.79 s | +1,265.93 |
| three desks always on | ⭐ 399.92 s | ⚠️ +11,735.34 |
⭐⭐ The policy keeps 60% of the flow benefit for 11% of the money. Always-on is the best plant to stand in and by far the worst to own — leaving the desk switched on around the clock costs 9.3 times what calling it up costs.
What a written-off lot would have to cost
The price sheet puts a write-off at 4. Raising that is one of the two prices that change the answer, and the answer is linear in it, because charges are priced over a finished run rather than fed back into it. At 48 instead of 4:
# edit `each` on entities.abandoned to 48.0 in BOTH models, then:
cwright compare inspection-desks.yaml inspection-desks-policy.yaml \
--kpi money --replications 10 --indifference 1.0
The verdict comes back InsufficientEvidence on a difference of −18.87, interval
[−524.17, +486.43] — straddling zero.
⭐ Break-even is a write-off costing about 47 — twelve times what this sheet charges. Above that the third desk pays for itself; below it, the queue is cheaper than the desk.
⭐ The other price that moves the verdict is the idle rate, which is what the policy is
spending. Priced at zero rather than 0.02 the same comparison reads Better by 238.40: if a
desk standing ready costs you nothing, call it up.
⚠️ Note how wide that interval is: ±505, against ±86 on the original money comparison. A bigger
penalty ties the money to abandoned, which is the most variable quantity in the run, so the
break-even point itself is only pinned to within about ±17. "Somewhere between 30 and 65" is the
honest reading, and it is still decisive — nobody thinks a written-off wafer costs 4.
Two things this example cannot tell you
This price sheet charges nothing for a lot that waits. The only route from flow to money
here is entities.abandoned, and that is why a plant 26% faster still loses. If work in progress
costs you money — capital tied up, a customer kept waiting — price it with
resources.inspect.queue, which counts lot-seconds in the
queue, and the policy's shorter queue starts earning directly.
The reneging is doing more than it looks. Writing lots off is what keeps the baseline's mean
cycle time down: the lots that would have waited longest leave the population instead of being
averaged into it. Remove patience_seconds from the baseline and its mean cycle time rises from
699 s to 770 s, so the effect is real.
Here it is small — about 29 lots of 4,019 — and shortening patience to 600 s, which writes off 6.4%, shows what it costs to ignore:
| patience | written off | two desks | third on call | money verdict |
|---|---|---|---|---|
| 1,800 s (shipped) | 29.5 | 700.31 s | 518.79 s | ⚠️ Worse by 1,266 |
| 600 s | 256.4 | 507.63 s | 481.62 s | ⭐ Better by 687 |
⚠️⚠️ The money verdict inverts. The cycle-time advantage does not reverse — it collapses from 26% to 5%, which is uninformative rather than misleading — but the plant that lost money at 1,800 s is the cheaper one at 600 s, because write-offs it avoids are now worth more than the desk it pays for. ⭐ So: check the write-off count before reading a cycle time, and re-read the money whenever patience changes.
Things worth trying
- Raise
eachonentities.abandonedpast 47 and watchmoneycross over. The verdict flips and nothing else in the model changes. - Set
servers: 3and deletecapacity_policy— a plain third desk, always on. ⚠️ Notup_to: 3on the resource: that key lives only insidecapacity_policy, and every type here refuses an unknown field, so you would get a parse error rather than a plant. - Lengthen
ready_after_secondsto an hour. Both the benefit and the cost come down with it — time in the plant goes to 602 s and the money penalty to 586, roughly half of each — so a slow desk is a smaller version of the same trade rather than the worst of both. - Price the idle desk at 0 and the write-off at 48, one at a time, and watch which way the verdict tips. Those are the two numbers the decision actually rests on.
Related
- Charges — the price sheet, and what can and cannot carry a price
- Capacity that follows the queue —
capacity_policyin full cwright compare— the three outcomes, and whyInsufficientEvidenceis one- A grocery checkout — the same shape of question with no money in it at all