FactorySemantics MES · a measured plant
Sixty million ids a day, every one observed
A disposable-cutlery manufacturer makes ten million forks, ten million spoons and ten million knives a day, stacks them, wraps each stack onto a plate and palletizes the wraps. Every piece, stack, plate and wrap carries its own id; a vision station judges every piece and every stack on four attributes; quality measures five dimensions every fifteen minutes; and every pallet leaves with a certificate of analysis. This is what the MES did with that plant, measured: for the people who run the line and its quality, and for the people who run the systems.
One desktop: Intel i5-12600K, 16 GB, NVMe. PostgreSQL 18 as a user service on the same machine, one database per run; the API one uvicorn process. Speed is the load multiplier — 1x is the plant's real rate. Each speed was run at least twice. Every figure below is computed from the runs' own databases, logs and results files by labs/cutlery/analysis/cutlery_run_analysis.ipynb; nothing is typed in.
The plant, and what flows through it
Nine moulding lines — three each of forks, spoons and knives — each ending in a vision station that judges every piece. Thirty-two stackers take a fork, a spoon and a knife from the lines' output and close a stack that its own vision station judges; two stackers feed every wrapper because a stack takes twice as long as a wrap; sixteen wrappers seal a stack onto a plate; four palletizers take 240 wraps to a pallet. The customer's arithmetic is 60,041,666 ids a day: 30,000,000 pieces, 10,000,000 stacks, 10,000,000 plates, 10,000,000 wraps and 41,666 pallets.
| Stream | Per day | Per second | How it enters the MES |
|---|---|---|---|
| forks, spoons, knives | 30,000,000 | 347 | one OPC UA group per piece: serial, four vision attributes, a pass word |
| stacks | 10,000,000 | 116 | one group per stack: serial, its three members, four attributes |
| plates | 10,000,000 | 116 | the wrap's group names the plate |
| wraps | 10,000,000 | 116 | one group per wrap: serial, the stack and the plate, four attributes |
| pallets | 41,666 | 0.5 | one group per pallet: serial and its 240 wraps |
| ids | 60,041,666 | 695 | 50,000,000 inspection groups a day |
How an id enters: one group notify, one event, one record
Nothing is minted from a count. The station is the source of truth: its vision system decides whether the piece is good, stamps the serial and the four readings with one source time, and publishes them as one OPC UA group. The agent subscribes to every inspection tag at full rate, assembles the tags that share a source time into one event, and writes it in bulk — the unit, its inspection row and, for a stack or a wrap, the containment of its members — with no HTTP in the path. A piece that fails never reaches a stack. OPC UA only notifies a value that changed, so a tag that repeats from one event to the next is filled from what the station last sent; a group whose serial never arrived is recorded as partial and counted, never dropped.
What the vision stations judged, and where the pieces went
Every piece carries four readings and a verdict; every stack and every wrap the same. The verdict is a pass word whose bits name the attribute that failed, so why a piece was scrapped is a query. The containment records say which marker's pieces went into which stacker's stacks, which stacker fed which wrapper, and what reached which pallet — and because a failing piece is never offered to a stacker, the number of stacks containing a failed piece has one right answer.
The dimensional checks, and the certificate a pallet leaves with
Every fifteen minutes an inspector measures five characteristics of each utensil through the same API a browser uses. A pallet's certificate of analysis states, for each characteristic, the checks inside the pallet's production window, the Cpk over them (sigma from the mean moving range) and whether the process was stable — and says no capability outright when the window holds fewer than the twelve points the method needs. Under that it lists every wrap, the stack and the plate in each, and every piece in each stack, rendered from the containment record and issued as an immutable document: 1,440 units under one pallet serial, gathered in 37.3 ms.
What the line did
The bookings say what each station made, the state intervals say what it was doing, and the scorecard says whether the MES saw the breakdowns the script put in — with a fault it recorded after its window withheld rather than scored missed.
For IT: the systems, the loads, and where they bend
Five processes and a database server stood in for a plant: an OPC UA server replaying the line's per-second truth and publishing the inspection groups; the OPC agent taking the groups, writing units and inspections in bulk, booking from counter deltas and recording states; PostgreSQL holding one database for the run; the API serving the floor, the screens and the certificates; and the operations loop being the people. Storage is a module: the same models and migrations run on one SQLite file for a small plant, and on PostgreSQL when a plant like this one switches it on.
| Run | Groups / s | Coverage | Partial | ms / batch | Agent CPU % | PostgreSQL CPU % | Replay CPU % | API CPU % | MB after the hour | Verdict |
|---|---|---|---|---|---|---|---|---|---|---|
| 1x | 552 | 100.00% | 0 | 128.6 | 23.2 | 5.0 | 12.8 | 1.3 | 1,254 | kept up |
| 1x | 551 | 100.03% | 0 | 128.2 | 22.9 | 5.5 | 12.8 | 1.2 | 1,248 | kept up |
| 1x | 550 | 99.94% | 0 | 129.6 | 22.5 | 5.5 | 12.5 | 1.2 | 1,237 | kept up |
| 3x | 1,668 | 99.68% | 0 | 128.9 | 56.4 | 15.7 | 37.6 | 2.6 | 1,177 | kept up |
| 3x | 1,669 | 100.17% | 0 | 129.7 | 56.5 | 15.7 | 37.6 | 2.6 | 1,165 | kept up |
| 10x | 2,149 | 48.24% | 0 | 728.5 | 71.8 | 22.8 | 92.9 | 5.1 | 593 | fell behind |
| 10x | 2,187 | 48.11% | 0 | 638.6 | 68.8 | 22.1 | 93.0 | 5.2 | 431 | fell behind |
| 5x | 2,808 | 99.84% | 0 | 277.1 | 72.9 | 24.8 | 62.4 | 3.1 | 1,112 | kept up |
| 5x | 2,787 | 99.42% | 0 | 314.7 | 72.9 | 25.5 | 62.3 | 3.9 | 1,106 | kept up |
| 8x | 1,349 | 30.27% | 0 | 650.5 | 64.3 | 19.1 | 93.9 | 5.3 | 333 | fell behind |
| 8x | 710 | 15.98% | 0 | 650.2 | 46.6 | 15.7 | 93.9 | 4.2 | 190 | fell behind |
Findings
Operations and quality
- Every id is an observed event: the stations published 1,982,899 inspection groups in the real-time hour and the agent recorded 1,981,758 of them — 0 partial, 0 duplicate, 0 naming a member it had never seen.
- A piece that failed vision is in the record with the attribute it failed on, and no stack contains one.
- The dimensional checks are the process's own record — 60 in the hour, five characteristics per utensil every fifteen minutes — and a pallet's certificate computes its Cpk over the checks inside that pallet's window, with the sample size stated.
- The certificate lists everything on the pallet, each wrap with its stack and plate, each stack with its fork, spoon and knife, and is issued immutable.
IT
- At the real rate the plant costs one desktop 22.5% of a core on the agent, 5.5% on PostgreSQL, 12.5% on the replay and 1.2% on the API. Memory is flat.
- The inspection path scales with rate until one agent process saturates at about 3,490 groups a second — roughly 6.0 times the customer's rate. The next step is a second agent process over a partition of the stations, not a bigger box.
- Storage grows 1236.6 MB an hour at the real rate, 29.68 GB a day; units, inspections and their indexes are almost all of it. That is the large plant's tier — PostgreSQL, partitioned by day, closed days archived to plain files — and a small plant never needs it.
- The recall questions do not grow with the database except the one that asks where a lot went that reached a third of everything made.
What the simulator cannot say
- Its stations' vision readings are drawn around a nominal; a real station's distribution has the shape the process gives it. The method — limits, a pass word, Cpk over a window — does not depend on the shape.
- The OPC UA server is a Python replay. It publishes what the plant would at 1x; at higher speeds the emitted count against the expected count says how much of the load it could produce, and only what was published can be scored.
Provenance
The notebook that produced every figure, the runner that produced the evidence, and the plant description are in the repository under labs/cutlery/. Re-running the notebook re-derives the page.
| Evidence | Where | Stamp (UTC) |
|---|---|---|
| scored run at 1x | labs/cutlery/out/tmp/fsmes-run-cutlery-jo5wii0c, database fsmes_run_20260906t204224z_1x | 20260906T204224Z |
| scored run at 1x | labs/cutlery/out/tmp/fsmes-run-cutlery-wl3ql8_g, database fsmes_run_20260906t214300z_1x | 20260906T214300Z |
| scored run at 1x | labs/cutlery/out/tmp/fsmes-run-cutlery-jf63pri8, database fsmes_run_20260906t233823z_1x | 20260906T233823Z |
| scored run at 3x | labs/cutlery/out/tmp/fsmes-run-cutlery-bwd8r0vm, database fsmes_run_20260907t003928z_3x | 20260907T003928Z |
| scored run at 3x | labs/cutlery/out/tmp/fsmes-run-cutlery-8wlfrc8w, database fsmes_run_20260907t010005z_3x | 20260907T010005Z |
| scored run at 10x | labs/cutlery/out/tmp/fsmes-run-cutlery-p0ir8zw1, database fsmes_run_20260907t014656z_10x | 20260907T014656Z |
| scored run at 10x | labs/cutlery/out/tmp/fsmes-run-cutlery-v07_u9rz, database fsmes_run_20260907t015329z_10x | 20260907T015329Z |
| scored run at 5x | labs/cutlery/out/tmp/fsmes-run-cutlery-a0jhqdei, database fsmes_run_20260907t020033z_5x | 20260907T020033Z |
| scored run at 5x | labs/cutlery/out/tmp/fsmes-run-cutlery-dlfl4b68, database fsmes_run_20260907t021309z_5x | 20260907T021309Z |
| scored run at 8x | labs/cutlery/out/tmp/fsmes-run-cutlery-10dkevce, database fsmes_run_20260907t022544z_8x | 20260907T022544Z |
| scored run at 8x | labs/cutlery/out/tmp/fsmes-run-cutlery-xmcsicm4, database fsmes_run_20260907t023347z_8x | 20260907T023347Z |
| simulator per-second truth | labs/cutlery/out/*.csv | generated by run_cutlery.py |