Technology · for reading on your own

A factory that evidences its interventions

Five plants, two protocols, a historised twin and a control loop that may change manipulated variables — but only after four instances in turn have had the chance to say no and one of them is a human. This page shows how that is built and exactly where the automation reaches into a machine.

5

plants, 22 units — four over OPC UA, one over Modbus/TCP

21

writable points out of 670 metrics. Everything else is read-only.

0

models in the control loop. The loop is rule-based — one is trained, it predicts tool life and proposes nothing.

250

tests, green. Most of them pin down a bug that was once real.

The cross-section

There is exactly one way up and exactly one way back. The way back is the same one a third-party control system would have to take — not a shortcut into the simulation.

Every layer computes only its own thing. The simulation knows no protocol, the controller knows no database, the control loop does not know a single plant by name — it knows Regelwerk objects (rule sets). That is why a fifth plant costs a module and an entry, not a rebuild.

LayerComponentTechnologyPort
0SimulationPython 3.12, own core — named random streams, deterministic
1Controllerasyncua 2.0 — real OPC UA server, browsable with UaExpert4840
1Legacy machinepymodbus 3.15 — Modbus/TCP slave, readable with mbpoll5020
2Edge gatewaypysparkplug via paho-mqtt, Eclipse Mosquitto 21883
2Modbus connectorpymodbus client, KPI calculation at the edge
3HistorianTimescaleDB on PostgreSQL 16 — hypertable and continuous aggregation5442
3APIFastAPI on uvicorn, asyncpg pool8021
3Control loopMAPE-K, cycle 20 s; ledger with Ed25519
4InterfaceSvelteKit with adapter-node, three.js for the hall view5180

Four plants speak OPC UA, one speaks Modbus. Both chains end in the same Sparkplug group, with metric names that are the idShortPath of the AAS — no translation step, no mapping table that somebody has to maintain. How the cables run is described under Data paths.

Where the automation intervenes

The automation intervenes at exactly 21 of 670 points — and even there only after four instances in turn have had the chance to stop it.

21 writable — feed, target speed, holding temperature 649 read-only

Writable is what the unit itself reports as a manipulated variable — not what is on a list. The plant additionally clamps its limits itself.

  1. M

    Monitor

    loop/monitor.py

    Reads the AAS twin, not the raw sensor data. Cockpit, control loop and third-party tools thereby see the same truth. Aggregated time-weighted, because the gateway only sends changes.

  2. A

    Assess

    loop/regelwerke.py

    This is where the domain knowledge lives, one per plant: on the line the bottleneck, in the cell the queue, at the filling line the back-pressure, at the reactor the exotherm. What comes out is a sentence a foreman would say.

  3. P

    Plan

    loop/plan.py

    Turns the finding into a limited proposal: a target value, a step, a predicted effect as a number. Without this forecast it could not later be checked whether the proposal was good.

  4. 1

    The gate

    can say no loop/policies/policies.yaml

    Checks against the policy and justifies every rejection individually: limits, step size, minimum confidence 0.55, at least 15 minutes of observation, 45 minutes of plant runtime, 20 minutes spacing — and a four-hour lockout for a target value that has already been rejected once. A YAML file, not code.

  5. 2

    The human

    Approval required

    Without approval nothing happens. The proposal lapses after 30 minutes because the situation it relied on is then no longer the current one. Who approved it appears in the evidence ledger.

  6. 3

    The trial run

    can say no loop/canary.py

    An approved change is not adopted but run on trial. Afterwards the measured effect decides, not the hoped-for one. Whatever delivers less than announced goes back. No grey area.

  7. 4

    The watchdog

    aborts

    Runs alongside during the experiment. If scrap rises above the limit or the OEE drops away, it aborts immediately. A safety event at the reactor is even checked before the grace period: KPIs fluctuate after a change, an emergency shutdown does not.

  8. 5

    The noise band

    indistinguishable

    A measured change that is smaller than the KPI’s natural scatter is neither evidence nor refutation. Booking it as a success would be a sales brochure, as a failure a statement the measurement does not support.

  9. K

    Execute and attest

    loop/execute.py

    The only write path: Sparkplug DCMD to the gateway, which puts it onto OPC UA — the same way a third-party control system would have to go. Every step before and after is appended, hash-chained and signed.

What comes out

OutcomeCountWhat it means
Rejected by the gateEvery rejection with an individual rationale: limits, step size, confidence, observation period, spacing. The gate is not a form.
ConfirmedEffect measured and sufficiently close to the forecast.
Rolled backThe trial run did not confirm the forecast — the old value came back.
Applied and standingPermanently adopted changes.
AutonomyThe policy requires confirmed forecasts before autonomy is even up for debate.

From /api/loop/bilanz — the numbers are not in the source code. In live operation they count over the entire runtime, in the public version over the recorded window; that is why they are smaller there.

Why no model

In the entire repository there is no torch, no sklearn, no tensorflow and no model call. The control loop is rule-based, and deliberately so.

The reason is in a comment above the rule sets: a model that says “set it to 1.06” is worthless at the acceptance meeting if nobody can explain why. What comes out here instead is a sentence a foreman would say — with KPIs beside it and a forecast against which the proposal must later be measured.

The second reason is metrological and weighs more: the scatter is larger than the effect. On the line, output fluctuates by ±14 % at an unchanged manipulated variable, at the filling line by ±25 %, at the reactors by ±77 %. A model that learns from such data learns the noise first. Before learning becomes meaningful, the measurement must get better — not the algorithm. How a model would dock on nevertheless is described under Model and docking point.

The evidence

Every finding, every proposal, every rejection with its rationale, every approval with a name, every verdict: one entry, appended, hash-chained, signed with Ed25519. The hash covers the form the database stores — the payload first runs once through jsonb, because numeric has no negative zero.

The chain check reports a break. It stays.

The cause was exactly this negative zero, found and fixed — not tampering. What happened is recorded as its own entry in the chain itself. An append-only record is not cleaned up, not even by its operator: at the meeting a visible break with an explanation is the better argument than a smooth chain. The check worked, and nobody tidied up. It can be seen under Control loop.

What it is not

The limits are stated here so that they are not found at the meeting.

Not a real plant

The physics is simulated

Taylor tool life, Arrhenius kinetics, MTBF/MTTR — plausibly parameterised and checked against synthetic cases, but not calibrated on a real machine. Protocols, twin and control loop, by contrast, are real.

Conformity

Two of thirteen submodel types follow IDTA templates

Nameplate and technical data. Operating state, OEE, manipulated variables and everything plant-specific sit under their own IRI — and the conformity report in the AAS Explorer states exactly that, instead of writing a foreign semanticId beside it.

Open

One plant cannot evidence its effect

In the CNC cell the manipulated variable acts on only one of five machines. The loop permanently says “indistinguishable” there — correct, but unsatisfying. The comparison against an unchanged sister unit is the next planned step.

Open

No AAS server, no evidence layer

The twin lives in the historian; a BaSyx server alongside would be the next conformity argument. Order and batch context down to the individual workpiece is planned but not started.

Check for yourself

In this project the rule is: where a number is asserted, the command to recompute it belongs beside it. That applies to this page too.

# Status and tests
systemctl --user status zenfactory.target
.venv/bin/python -m pytest -q                      207 passed

# The twin, live
curl -s localhost:8021/api/anlagen | jq
curl -s localhost:8021/api/ledger/verify | jq

# The protocols raw — without ZenFactory in between
scripts/opcua_browse.py --watch ST020              OPC UA
mbpoll -a 1 -r 1 -c 15 -1 127.0.0.1 -p 5020        Modbus

# One plant's operating point, measured reproducibly
.venv/bin/python scripts/sweep_arbeitspunkt.py REAKTOREN --stunden 96 --seeds 3
QuestionFile
What may the control layer do?loop/policies/policies.yaml
Where does the domain knowledge per plant come from?loop/regelwerke.py
Where is the single write path?loop/execute.py
What does the twin look like?aas/models/
What is on the wire of the legacy machine?config/registerkarte_zs100.yaml
Which assumptions are in the euro amount?config/wirtschaft.yaml
How is the whole thing put together?docs/ARCHITEKTUR.md
What happens on a restart?docs/BETRIEB.md
Why is something the way it is?docs/journal/decisions.md