Operators
Any unfamiliar word is in the glossary.
An operator pool is people as a constraint. A machine may be free and the work still wait, because nobody is there to load it.
operators:
- { id: setters, count: 2 }
and on a step:
resource: mill
process_seconds: { kind: constant, value: 300 }
operator:
pool: setters
attendance: { load: 1.0, unload: 1.0 }
A plant whose tools look under-utilised and whose work is still late is usually short of people, not machines. Modelling operators is how that becomes visible instead of arguable.
The pool
| key | type | required | what it means |
|---|---|---|---|
id | string | yes | The pool's name, referenced by a step's operator. |
breaks | array of break | no | Breaks, meals and training slots. ⚠️ A person on a break is unavailable, not busy. Modelling it as work would make a short-staffed pool look more utilised when it is in fact less available, which are opposite readings of the same fab. |
count | integer | yes | How many people. ⚠️ Zero is refused: a pool nobody can be taken from stops every step that needs it for ever, which reads as a fab that produces nothing rather than as a model error. |
travel_seconds | dist | no | How long somebody takes to reach the next job. See walking time below. ⚠️ A declared zero is refused: omit the key instead. |
roster_profile | string | no | A profile id whose factor is how many of them are on shift. ⚠️⚠️ A headcount, not a multiplier, and refused if it is not a whole number or exceeds count. ⭐ This is deliberately not a break: a break is staggered so a pool never goes to lunch together, and a roster wants precisely what that prevents — everybody off at once, phase-locked to a cycle. ⭐⭐ A person finishes their task before going off — and so does a rostered-off machine, which is why the distinction is roster versus downtime rather than people versus machines. |
Walking time
The tools in a fab are tens of metres apart, so somebody who has just finished a job is not
available for the next one at the same instant. travel_seconds says how long the walk
takes.
⚠️⚠️ It is charged to the person, not to the lot — which is the whole difference between
this and a step's transport_seconds. Transport moves the lot and occupies
nothing else; a walk occupies the person on their way to whatever comes next. So a tool's
own time per lot does not change when you add a walk, while the pool gets scarcer, and lots
then wait longer for a person. Measured on tests/corpus/job-board.json with a 20–40 s
walk: the bench's WaitingForLabour goes from 0.82% to 1.78%, the pool's peak_queue
from 2 to 4, and the bench's own utilisation does not move at all.
⚠️⚠️ A walk is charged per attended phase, not per lot. The attended part of a phase
comes first — the operator starts the job and leaves — so a step that attends load, process
and unload seizes and releases a person three separate times, and they cross the fab
three times. Measured: a pool reads 21.25% where the per-lot reading predicts 13.75%. ⭐ A
phase with no duration takes nobody and therefore costs no walk, however large its
attendance fraction.
⭐ It applies to every job the pool does, maintenance and repair included, because a technician walks too.
Why it is a duration and not a resource
A hold on labour has a fixed duration and acquires nothing else — that is the property which makes seizing a machine and a person at the same time safe without any lock ordering, and it is documented in the engine's own labour module. A walk keeps it: the length is drawn the moment the person is let go, so the hold is still a known duration, and walking acquires nothing.
⚠️ A walk that had to reserve something — a corridor, a vehicle, an airlock — would break that argument and reintroduce the deadlock the design avoids by construction. Contended transport for people is not expressible this way, deliberately.
Shifts and breaks
breaks take the pool out of service on a repeating cycle.
| key | type | required | what it means |
|---|---|---|---|
name | string | yes | A label for this break, so several can be told apart. |
every_seconds | number | yes | Seconds between the starts of successive occurrences — a fixed grid, the same convention as calendar maintenance. |
duration_seconds | dist | yes | How long the break lasts. |
first_at_seconds | number | yes | When the last person of the pool has their first one. ⚠️ Occurrences are staggered evenly across the pool: two operators with a first_at_seconds of 360 minutes go at 180 and 360. This is the only anti-synchronisation mechanism there is, and without it a pool goes to lunch together and the fab stops twice a day — while still looking like a plausible model. |
Which phases need a person
attendance says when during a step the operator is held: load, process, unload.
Holding somebody for the whole of a long process when they are only needed to load and
unload it will invent a shortage that is not there.
⚠️ The key is pool (singular) and it takes either one pool name or a list; a
list means any of these will do. The values are fractions in [0, 1], not flags:
1.0 holds a person for the whole phase, 0.5 for half of it.
| key | type | required | what it means |
|---|---|---|---|
pool | pools | yes | Which pool, or which pools are acceptable. |
attendance | attendance | no, default a full hold | ⚠️ An object, not a number — { "load": …, "process": …, "unload": … }. A bare number is refused: invalid type: floating point, expected struct AttendanceIr. Fields below. |
attendance
What fraction of each phase the person is held for. ⚠️ Fractions in [0, 1], not switches:
"process": 0.5 ties the operator up for half the processing time, not for every other job.
load: 1.0
process: 0.0
unload: 1.0
That is the common case — the operator loads the machine and unloads it, and walks away while it
runs. ⚠️ Omitting attendance holds the person for the whole operation, which is a far
stronger constraint than most models intend.
| key | type | required | what it means |
|---|---|---|---|
load | number | no | Fraction of the load phase, in [0, 1]. |
process | number | no | Fraction of the process phase, in [0, 1]. |
unload | number | no | Fraction of the unload phase, in [0, 1]. |
Pools for transport
transport_operator requires a person for the move between steps, not for the step itself.
| key | type | required | what it means |
|---|---|---|---|
pool | pools | yes | Which pool, or which pools are acceptable. |
attendance | number | no, default 1.0 | Fraction of the delay a person is needed for, in (0, 1]. The attended part comes first — the operator starts the move and lets it finish. |
Types used above
pool
Which pool a step draws its person from. Either one name:
pool: technicians
or a list, meaning any of these will do:
pool:
- technicians
- supervisors
The key is pool (singular) in both cases, whether you give it one name or several.
break
A recurring interruption when the whole pool stops — lunch, a shift handover.
name: lunch
every_seconds: 28800.0
duration_seconds: { kind: constant, value: 1800.0 }
first_at_seconds: 14400.0
| field | meaning |
|---|---|
name | what to call it, so it can be told apart in the output |
every_seconds | how often it comes round |
duration_seconds | how long it lasts, as a distribution |
first_at_seconds | when the first one happens |
first_at_seconds is required, and it matters more than it looks. If every pool takes its break
at the same moment, the whole plant stops twice a day — which may be exactly right, or may be an
artefact of everybody defaulting to zero. Stagger them deliberately.