Machine Inspector: model notes
Machine & Inspector
GPSS Example #3: a machine produces one part every 5 minutes and
hands it to a single inspector. 15% of inspected parts fail and are
scrapped; 85% pass. The reference model for probabilistic outcome
recording (the GPSS TRANSFER .15,,Reject block) and for
deterministic arrivals.
Problem
Constant production: one part every 5 minutes, flat, for an 8-hour
shift (96 parts). Each part enters a single-server inspection queue
(Uniform(1..7) minutes). At end of inspection, the part is marked
accepted or rejected with probability 0.85 / 0.15. The model reports
counts, wait times, and inspector utilization; the observed reject
rate should hover near the target 15% with noise bounded by sample
size.
GPSS reference: docs/archive/gpss-examples.md
example #3. Original GPSS transcribed at the top of
machine_inspector.odin.
Model in this directory
Part(machine_inspector.odin:80-138) — four-state machine (Arriving / Waiting / In_Inspection / Done). Uses the peek-then-seize fast path from the barbershop plus a dispatcher call at completion (hybrid of the barbershop and the tool crib).Machine(machine_inspector.odin:166-190) — the generator. Distinct from the other examples: no RNG draw for inter-arrival, justeng.advance(..., minutes(Minutes(5)))on every tick. A constant-rate generator is a normal puck whose schedule is deterministic.dispatch(machine_inspector.odin:153-162) — identical-in-shape to the tool crib's dispatcher, minus the priority concern. Called fromIn_Inspectionon completion.
Shop (machine_inspector.odin:56-69) holds:
inspector: sim.Facilitywait_list: sim.Set(FIFO)- Two independent RNGs:
svc_rngfor inspection time,outcome_rngfor the accept/reject Bernoulli draw - Counters, totals, and utilization accumulator (
total_svc_time)
Why this shape
The "TRANSFER .15" branch is a stat decision, not a flow decision.
GPSS TRANSFER .15,,Reject reads as two distinct transaction paths,
one terminating at Reject, one at TERMINATE. In practice, both
paths just count the part and exit — no further processing
happens on either branch. The puck model collapses this into a
single post-service moment where a Bernoulli draw picks which
counter to increment
(machine_inspector.odin:119-125):
u := db.random_real(&shop.outcome_rng)
if u < REJECT_PROB {
shop.rejected += 1
} else {
shop.accepted += 1
}
If the two branches had different behavior (rework, re-queue,
alternate routing), the decision would move earlier and the flow
would actually fork — that's the threshold_routing shape. The
principle: branch in flow only when the downstream work differs;
otherwise branch only in the stat.
Two RNG streams, by purpose not by quantity. svc_rng drives
service time; outcome_rng drives accept/reject. Both could be
collapsed into one stream — each random_real call would still be
independent — but separating them means a seed sensitivity analysis
on service-time distribution does not perturb the reject pattern,
and vice versa. This is the same principle as the barbershop's
two-stream arrangement, applied for a different reason (stat
decorrelation rather than arrival/service decorrelation). See
modeling guide §5.
Deterministic arrivals are just eng.advance with a constant.
No RNG, no distribution, no Poisson process. Machine shows that
"generator" is not a special primitive — it's a puck with a
specific tick pattern. For a paced production line this shape is
cleaner than rigging up a Uniform(5, 0) degenerate distribution.
Peek-then-seize and dispatcher, both. This model uses:
- The barbershop's fast path in
Arriving(skip queue if inspector free and nobody waiting). - The tool crib's
dispatchhelper on completion.
This combination is stable across the three worker-queue examples
(barbershop, tool_crib, machine_inspector) and is the main
candidate for extraction into a sim.worker_cycle(...) helper once
a few more variants land. See
pruning-status.md §"Patterns
emerging."
Utilization is measured by summing service durations, not by
inspector-busy sampling. total_svc_time += svc at each service
start gives an exact numerator; dividing by END_TIME gives
utilization. This is cheaper and more precise than
on-step sampling, at the cost of being tied to the
completed-services count (in-flight services at shift end are
not credited). For an 8-hour shift with 1–7 minute services the
truncation bias is negligible; for shorter runs or longer services,
prefer an area-under-curve integration like
mm1.
Alternatives considered
Model the accept/reject paths as separate terminal states
Part could enter .Accepted or .Rejected instead of .Done,
each with its own stat-collection logic. For this model that adds
two enum values and no information — the decision is instantaneous
and post-service. If the two outcomes diverged in behavior (rework
loop, disposition delay, failure-mode tabulation), the extra states
would earn their keep.
Use db.d_uniform from sdidb
The model defines a local uniform(gen, mean, half) helper rather
than calling db.d_uniform (which has the same signature). The
local form predates the sdidb convenience layer; it's not wrong,
just not yet migrated. A future pass will consolidate on
db.d_uniform across the examples.
Branch in flow (re-queue rejects for rework)
A plausible extension: rejected parts go back to the inspector for
a second pass, or to a rework station. That model does fork flow,
with a new state transition from In_Inspection back to Waiting
(or to a separate rework facility). Worth noting as the natural
next step if the example set needs a rework variant.
A single puck combining machine and part
In GPSS the machine is just GENERATE 5, not a transaction. We
could elide the Machine puck and spawn parts from a free-running
scheduler hook. We don't, because keeping the machine as an
explicit puck makes it trivial to add machine behaviors later —
breakdowns, shift changes, variable production rate — without
restructuring.
What this example teaches
This is the reference for:
- Probabilistic outcome as stat decision, not flow decision
(GPSS
TRANSFERstatistical mode) - Deterministic generators via
eng.advancewith a constant - Separate RNG streams by purpose (service vs outcome)
- Utilization by service-time accumulation (cheap, exact for completed work)
- Combining peek-then-seize with an explicit dispatcher in one worker queue — the hybrid worker-queue shape that recurs across examples
CLI
./machine_inspector [options]
Add --json to emit the uniform envelope (metadata, execution_stats,
metrics, details) instead of the default text output.
| Flag | Type | Default | Description |
|---|---|---|---|
--json |
bool | false |
Emit uniform JSON envelope instead of text. |
Example runs:
./machine_inspector # default text run
./machine_inspector --json # uniform envelope
Edit constants at the top of machine_inspector.odin to vary
interval, inspection distribution, reject probability, or shift
length — those are not exposed as CLI flags.
Running it
odin run examples/machine_inspector
Default output is a console summary with counts, reject-rate check, wait stats, sojourn, and utilization.
See also
- Modeling guide §5 — independent RNG streams
- Modeling guide §9 — utilization and time-weighted stats
barbershop— source of the peek-then-seize fast pathtool_crib— source of the dispatcher pattern; this model is the FIFO-priority specializationthreshold_routing— contrast: a model where the post-service branch does fork flow, not just statspruning-status.md, commitcf45491— the Phase B commit that added this example