Products and steps
Any unfamiliar word is in the glossary.
A product is a thing you make and the ordered route it takes. A step is one operation on that route.
id: bracket
pieces: 25
steps:
- resource: saw
process_seconds: { kind: constant, value: 60 }
- resource: mill
process_seconds: { kind: erlang, k: 4, mean: 300.0 }
- resource: inspect
process_seconds: { kind: uniform, lo: 30, hi: 90 }
An entity walks these in order. When it runs out of steps, it is complete.
Product
| key | type | required | what it means |
|---|---|---|---|
id | string | yes | The product's name, referenced by releases and by the schedule. |
steps | array of step | yes | The ordered route. An entity visits these in sequence, and may revisit via rework. |
pieces | integer | no, default 1 | Pieces per entity — wafers per lot. Read by batching, scrap, a per-piece step time (process_per_piece, piece_interval_seconds) and maintenance that counts pieces; every other quantity in the model is per entity. |
pieces is what makes an entity divisibleAn entity is a lot of pieces. Scrap can take individual pieces and leave the rest, a
batch can be sized in pieces rather than lots, a step can be timed per piece, and maintenance
can count pieces. A schedule entry may give one lot its own size. Every other quantity in the
model is per entity.
Step
Only resource and process_seconds are required. Everything else adds a mechanism.
| key | type | required | what it means |
|---|---|---|---|
resource | string | yes | Which resource performs it. |
process_seconds | dist | yes | How long the work itself takes at this step. A zero constant is legal and means an instantaneous step. |
setup | string | no | The setup this step needs. None means any. |
setup_always | boolean | no, default false | Perform setup on every entity, even when the server is already in it — a recalibration rather than a change. Priced by the resource's changeover from any setup (from: "") to this one, which must exist. ⚠️ Only that row: an explicit from into the same setup prices a change, never this. |
patience_seconds | dist | no | How long an entity will wait in this step's queue before abandoning it. ⚠️ Abandonment is not scrap. The entity leaves the queue and the model, counted in Outcome::abandoned; it is not destroyed at a step and it does not continue. A model that silently kept them would report a fab whose customers all wait for ever. ⚠️ Only meaningful where something waits. An entity already in service cannot abandon — that is a different mechanism, and no queueing result this project checks against assumes it. |
operator | operator | no | A person this step needs, as well as the tool. |
probability | number | no, default 1.0 | Probability in [0, 1] that the step happens at all. Metrology sampling. |
name | string | no | A human-readable label, carried through to output. |
rework | rework | no | Send the entity back to an earlier step, or away on a sub-route, with some probability. |
dedication | dedication | no | Bind this entity to a server, or require a previously bound one. |
queue_timer | queuetimer | no | A ceiling on the wait between finishing here and starting a later step. |
consumes | array of consumes | no | Consumables this step draws down. ⚠️ Drawn before processing begins and never returned; an entity that cannot get its materials waits at the step rather than occupying a server while short. |
produces | array of produces | no | Consumables this step puts back, applied when the step completes. ⚠️ At completion, not at start — a half-stocked shelf is not stock. |
commissions | array of commissions | no | Machines this step puts into service when it completes — see Building capacity. |
transport_seconds | dist | no | Time to travel to this step, before queueing for a server. ⚠️ A delay, not tool time. Folding it into process_seconds was tried and charged every tool 7.5 minutes per visit: SMT2020's HVLM fab went from 41,681 lots per two years to 30,033 and its cycle time from 26 days to 238. Transport occupies the lot, never the equipment. |
start_interval_seconds | number | no | The soonest the station may start ANOTHER lot after starting this one. ⭐ Lot-level cascading, the same shape as a wafer-level part interval. With a resource capacity of c, a saturated station delivers one lot every max(start_interval, process / c). ⚠️ It belongs to the STEP because the interval is a property of the operation being run, not of the machine — although in SMT2020 the ratio process / interval turns out to be constant within a tool area, which is how the column was recognised. |
process_per_piece | boolean | no, default false | process_seconds is per piece: the drawn time is multiplied by the pieces the entity carries — a lot's own, when a schedule entry sets them. ⚠️ Not on a batch step, whose time is per batch. |
piece_interval_seconds | number | no | A wafer cascade: the entity's pieces pass through the tool this far apart. The step takes process_seconds + (pieces − 1) × interval, and on a station of capacity above 1 the next entity may start pieces × interval later. ⚠️ Not beside start_interval_seconds or process_per_piece. |
transport_operator | transport_operator | no | A person who must accompany the lot during transport_seconds. |
scrap | scrap | no | Work destroyed at this step. ⚠️ Scrap is loss, not rework: the pieces do not go anywhere and must show up in the conservation check. |
batch | batch | no | Process several entities together. Absent means one at a time. ⚠️ Entities batch only with others at the same step of the same product, which is what SMT2020's crit_sameroutestep specifies. A batcher that pools across steps would fill furnaces far faster than the real one. |
scale_profile | string | no | A profile id whose factor multiplies this step's process duration. ⚠️⚠️ The reciprocal of a release's rate_profile, which is why they are separate keys: 1.6 here means takes 60% longer, while 1.6 on a rate makes gaps shorter. ⭐ A duration that spans a segment boundary is integrated, not sampled at its start — the job carries a fixed amount of work and accrues it at 1 / factor(t) per second. |
Scrap
scrap destroys work at this step, in one of two ways. entity takes the whole job;
piece judges each piece independently, which thins the job rather than removing it.
| key | type | required | what it means |
|---|---|---|---|
entity | number | no | Probability the entire entity is destroyed after this step. |
piece | number | no | Probability each remaining piece is destroyed after this step. |
A "piece" is whatever the product's pieces counts: bottles in a crate, panels on a pallet,
wafers in a lot. A crate of 12 bottles with "piece": 0.05 usually arrives as a crate of 11 or
12; with "entity": 0.05 it occasionally does not arrive at all.
These were called lot and wafer until 2026-09-14. Both spellings still parse, so existing
models keep working, but the semiconductor names were a claim that this engine is only for
semiconductors — and it is not.
Two steps at 5% each lose 1 - 0.95² = 9.75%, not 5%. Each step judges the pieces that
survived the last one.
Rework
rework sends an entity somewhere else, with some probability. There are two shapes, and a
step declares exactly one of them.
| key | type | required | what it means |
|---|---|---|---|
probability | number | yes | Probability in [0, 1] of reworking. |
to_step | integer | one of the two | The step index to return to. ⚠️ Must be earlier than this one, or the route would move forward and the "rework" would be a skip. |
excursion | array of step | one of the two | A sub-route to run instead, resuming at the step after this one. |
to_step is a loop: the entity goes back over operations its own route already contains
and walks forward again, so the number of extra visits is geometric.
excursion is a detour: the entity leaves the route, runs a sub-route that may use tools
the route never visits, and comes back to where it left off. It redoes nothing, so it runs at
most once per visit to the step that sent it.
resource: align
process_seconds: { kind: constant, value: 90.0 }
rework:
probability: 0.08
excursion:
- resource: strip
process_seconds: { kind: constant, value: 120.0 }
- resource: coat
process_seconds: { kind: constant, value: 60.0 }
- resource: develop
process_seconds: { kind: constant, value: 45.0 }
A lot that fails alignment is stripped, re-coated and re-developed — three operations the
main route never performs — and then resumes at the step after align.
Photolithography rework is the case this exists for. The SEMATECH/MIMAC datasets hold their
rework as named sub-routes in a separate file: set3's A1090 is
COAT5 → PAL → DEV → SCOP2, four operations on four tools, and all 116 of that
dataset's rework steps point into such a sequence. No to_step can express it, because
to_step can only name a step the route already has.
Sending work back to a step that can send it back again is a loop with a probability, not a guarantee of termination. The engine reserves a distinct random block per revisit so a lot does not redraw the same outcome for ever, but the model is yours to make sensible.
An excursion does not have this hazard — it resumes past the step that sent it — but a
step inside an excursion may not itself declare rework, and the document is refused if
it does. The sub-route is not expanded again, so such a field would be accepted and then do
nothing.
Drawing material
consumes makes a step draw from a consumable. If the store cannot meet
the quantity, the entity waits here until it can.
| key | type | required | what it means |
|---|---|---|---|
consumable | string | yes | Which store this step draws from, by id. |
quantity | dist | yes | How much is drawn per entity. The step waits if the store cannot meet it. |
Putting material back
produces is the mirror of consumes, and together they are what makes a second tier
expressible. A step that draws a case from the storeroom and puts it on the shelf is an
ordinary product with an ordinary route — so queueing for the stacker, competing with other
duties, and the time it takes all fall out of mechanisms that already exist.
That matters because a single store conflates two losses with opposite remedies:
| an article is | whose problem | which lever |
|---|---|---|
| on the shelf | — | — |
| not on the shelf, but in the storeroom | shelf-stocking | another stacker |
| not ordered at all | purchasing | a higher reorder point |
With two stores they are simply stockouts on different consumables, and the results document reports them separately.
Production happens at completion, and before the scrap and rework decisions.
A step that drew its materials and finished the work put those units somewhere, whatever becomes of the entity next. Producing after the scrap roll would let a destroyed restock task delete stock it had already taken out of the storeroom.
⚠️ A store never exceeds its capacity, so production that does not fit is lost at the
cap and reported as total_spilled. validate refuses the case it
can see in the document — a constant quantity larger than the store itself — but a
distribution's upper tail is a property of the run, so the rest is counted rather than
refused.
Building capacity
commissions makes the completion of a step add machines to a resource that declared fewer
installed than servers. A tool installation in a fab ramp and a building in
a factory game are the same thing: an ordinary step that can draw its materials, need a person and
travel, whose completion is the moment capacity grows.
resources:
- id: assembler
servers: 4
installed: 1
- id: crew
servers: 1
products:
- id: build-assembler
steps:
- resource: crew
process_seconds: { kind: constant, value: 3600.0 }
consumes:
- { consumable: frames, quantity: { kind: constant, value: 8.0 } }
commissions:
- { resource: assembler, servers: 1 }
⚠️ Stations come into service lowest index first, each the instant the step completes, and work
waiting for them starts at once. A commission that finds nothing left to install is counted as
commissions_unfilled, never silently dropped.
⚠️ An uninstalled station is NonScheduled — where SEMI E10 books installation — so it is outside
the availability denominator. A group with uninstalled stations may not also have a
roster_profile, a capacity policy or downtime: each would bring a machine that is not yet built
into service, or into failure.
⚠️ A step that commissions may not also batch, scrap or rework: a batch finishes every
member, a scrapped entity still completes the step and rework repeats it, so each would build once
per entity or per pass rather than once per building. validate refuses the combination.
Types used above
operator
A person who must be present for this step, drawn from an operator pool.
pool: technicians
attendance: { load: 1.0, process: 0.0, unload: 1.0 }
pool takes either one pool name or a list, in which case any of them will do.
attendance says what fraction of each phase the person is held for, as a number in
[0, 1]. The example above is the common case: the technician loads the machine and unloads it,
and walks away while it runs. 1.0 for all three holds them for the whole operation.
These are fractions, not switches. "process": 0.5 means the operator is tied up for half the
processing time, not that they attend every other job.
dedication
Forces an entity back onto the same physical machine it used earlier — a tool lock.
chain: litho
mode: bind
| mode | meaning |
|---|---|
"bind" | remember whichever server ran this step, under this chain name |
"use" | run on the server remembered for this chain |
"use" does not fall back. If the remembered machine is down, the entity waits for it, because
that is what a real tool lock does. This can idle a whole bank of free machines, which is
correct and is also worth knowing before you model it.
queue_timer
A maximum time an entity may spend between this step and a later one — a bake-to-etch limit, a shelf life, a cure window.
until_step: 7
max_seconds: 14400.0
until_step is the index of the later step in the route, counting from zero. Breaches are
counted in the results document as timer_violations.
consumes
Material this step draws down, from a consumable.
consumable: adhesive
quantity: { kind: constant, value: 4.0 }
quantity is a distribution, not a fixed number — real consumption
varies. Use constant if it genuinely does not.
A step may list several. If any of them is short, the entity waits, holding the ones it has already drawn.
commissions
Machines a step puts into service when it completes.
resource: assembler
servers: 1
| key | type | required | what it means |
|---|---|---|---|
resource | string | yes | The resource that gains machines, by id. It must declare installed below servers. |
servers | integer | yes | How many machines come into service. 0 is refused — omit the entry instead. |
produces
Material this step puts back, into a consumable.
consumable: shelf
quantity: { kind: constant, value: 24.0 }
Same shape as consumes, and a step may list several. The quantity is a
distribution — a picker who sometimes drops one is a real thing to
model.
⭐ A step may consume and produce at once, which is the whole point: drawing a case from
storeroom and producing it into shelf is one step that moves stock between two tiers.
transport_operator
A person who must accompany the entity while it travels — a driver, a crane operator.
pool: drivers
attendance: 1.0
Only meaningful alongside transport_seconds; validate refuses one without the other. Here
attendance is a single fraction of the journey, not the three phases a machine step has.
batch
Process several entities together as one load. Fully described on the batching page.
min_pieces: 30
max_pieces: 60
group: cure
Sizes are in pieces, not entities, because that is what the equipment constrains — an oven
holds 150 panels whether they arrive as six pallets of 25 or five of 30. ProductIr.pieces
converts.