Use rotational discharge for Bento consumption

This commit is contained in:
2026-09-07 07:59:48 +02:00
parent be8d76507a
commit 0210ced9e3
10 changed files with 400 additions and 92 deletions
+127 -49
View File
@@ -1,60 +1,138 @@
# Bento 1 fresh bentonite consumption
`config/bento1-material-consumption.yaml` defines
`bento1-fresh-bentonite-consumption`. This KPI estimates **fresh bentonite
consumption** from the two fresh-spreader setpoints in g/m². The third spreader
uses recycled/recovered bentonite and is deliberately excluded; it is neither
missing data nor estimated. This is not total bentonite deposited on the product,
and setpoints are not a measurement of actual mass flow.
`bento1-fresh-bentonite-consumption`, an estimate of **fresh bentonite consumption**
from actual spreader roll speeds. Both needle rolls apply sequentially across the
full web width. The recycled/recovered third spreader is excluded.
The generic `area_application` source sums the configured application signals
and converts them to kg/h using `sum(g/m²) × nominal_width_m × speed_m/min × 60 / 1000`.
The existing previous-value integrator then accumulates kilograms only while
speed is strictly greater than 0.3 m/min. `direct_mass_rate` remains the default
for existing configurations, including K7. The persistent state schema and
production-order bootstrap/resume behavior are unchanged; each disjoint run
starts with a new sample baseline while retaining order totals.
The generic `rotational_discharge` source converts one or more rpm signals to the
existing integrator's kg/h: `sum(rpm) × nominal_width_m × specific_discharge_kg_per_rev_m × 60`.
Bento config sets the factor to **3.12 kg/(rev·m)** and uses:
Width comes from the existing ERP workplace status adapter and generic nominal
width parser. The ERP order must format to the ENLYZE order using
`Bento 1-{production_order}`. Missing/mismatched ERP context or an unparseable
width rejects the polling cycle without saving state. ENLYZE product metadata is
not used for width. The current ERP context supplies width for all runs of that
same order; this assumes the order's article width stays constant. Historical
orders no longer present in current ERP workplace status require a separate
historical context source for offline replay.
- Right ACT: `6e5d2d94-98f9-4cc1-8a88-7987c6282525`
- Left ACT: `cd7385c4-337b-4759-ab32-45d65beaf190`
Run with:
ENLYZE now returns physical rpm directly (scaling factor 1.0); no division by
1000 is applied. For 3.739 + 3.956 rpm and 5 m width, the rate is 120.042 kg/min
(7202.52 kg/h), giving 20.007 kg in ten seconds.
```sh
production-analytics run material-poll \
--config config/bento1-material-consumption.yaml \
--erp-secrets-file secrets/erp.env
Transport speed `fef41976-1103-4090-b780-eaeecc02fdfa` is exclusively the
production gate: speed must be strictly greater than 0.3 m/min. It does not
multiply the mass rate. The generic rotational mode also supports omitting
`gate_signal_ref` and `gate_threshold`, in which case all valid intervals are
active. Bento retains its gate.
The two old SET sources `19ea65d2-bd35-4d02-89de-4495583d9026` and
`d9f47615-69cf-4f97-88df-199585764491` are no longer requested by Bento.
`direct_mass_rate` remains the default for K7; `area_application` remains
available for other configurations. There is no Bento branch in the integrator.
The previous-value integration, JSON state format and order bootstrap/resume
remain unchanged. Totals accumulate across disjoint runs, with a new baseline
at each run boundary, even if the inter-run gap is under 20 seconds. Intervals
up to 20 seconds use the preceding rate and gate; longer sample gaps and
unobserved run tails are not integrated. Missing/nonfinite source values reject
the cycle. Finite negative speeds retain the existing generic signed-rate
semantics; no clamping is introduced.
Width comes from the existing ERP nominal-width parser, with workplace
`Bento 1` and order format `Bento 1-{production_order}`. Workplace comparisons
use `strip().casefold()`; order matching remains strict. Missing/mismatched ERP
context or an unparseable width rejects the cycle without saving state. The
current ERP context supplies width for all runs of the same order, assuming
constant article width. Historical orders no longer in the ERP workplace view
need a historical width source for replay.
The configured factor is based on the supplied independent historical checks:
3.1175 at 5.00 m (article 180305), 3.1193 and 3.1187 at 4.85 m (article 173000),
and median 3.1208 at 5.00 m (article 180005; p05–p95 3.0703–3.1558).
Tests use realistic rpm values and synthetic samples, including disjoint-run
bootstrap and persisted resume. They are not a new replay of recorded ENLYZE data.
## Old calculation cleanup: review before execution
No production state or database rows were changed during this implementation,
and no service start was requested. The operator reports Bento stopped;
sandbox systemd access could not independently confirm that state.
The deployed `/etc/systemd/system/production-analytics-bento1.service` specifies
`/opt/git-projects/production-analytics/data/state/material` as its state directory.
The existing state file for machine `5f42a4f6-9ca0-4f6f-9786-40d50a35b230`
and order `Bento 1-12026000857` is exactly:
```text
/opt/git-projects/production-analytics/data/state/material/4d6cc209e35bc58887e5b4a3816835006425429e810e0d957f651971723e8c2f.json
```
`erp_workplace: Bento 1` assumes that exact ERP workplace identifier; verify it
against the deployment's workplace view. The 20-second maximum sample gap is a
generic policy shared with K7, intended for a nominal 10-second sampling cadence.
One missing sample is tolerated: the resulting 20-second interval is integrated
using the preceding rate and gate. Gaps greater than 20 seconds are not
integrated, so prolonged or repeated gaps can undercount consumption.
Missing/nonfinite values from either configured fresh spreader reject the cycle.
Intervals exceeding the maximum gap and unobserved run tails are not inferred.
This identity was verified using the store's SHA-256 of the JSON machine/order
pair. The file exists but its contents are not readable by the sandbox user.
The other state file, `6d11088159b8e4fd21543560a54a31a0f86faf22bbf8f16873b228d1903c23f9.json`,
has not been attributed here and must be left untouched.
Before implementation, the calculation was independently validated manually
against the same three real ENLYZE signals over the two runs below, yielding
approximately **147021.9 kg** of fresh bentonite for `Bento 1-12026000814`.
The post-implementation live replay could not be repeated because ENLYZE
connectivity was temporarily unavailable (DNS resolution failure), not because
of a known implementation issue. Exact production-path replay remains a
follow-up verification rather than a blocker for this milestone.
Old PostgreSQL snapshots are in `material_consumption_snapshots`, scoped by both
`calculation_id = 'bento1-fresh-bentonite-consumption'` and
`machine_id = '5f42a4f6-9ca0-4f6f-9786-40d50a35b230'`.
The table has no calculation-source/version provenance column. Given the stopped
service and no rpm deployment yet, existing rows in this scope belong to the old
calculation. PostgreSQL was unreachable from the sandbox, so exact row counts,
order/run membership and timestamp bounds remain unverified. Do not mistake
this predicate for a completed row inventory. Review the following output first.
The regression uses article 180305, description `Bfix NSP 5300, 5,00 x 40 m`
(width 5.00 m), and the supplied boundaries of runs
`b675818c-4636-4976-be6c-3e0e24da9e8a` and
`25d67424-8346-42d4-b844-75b4097382bd` for order `Bento 1-12026000814`.
It constructs synthetic samples from the supplied aggregates: 1256.0 active
minutes, 31294.6 m² and 4698.0 g/m², yielding approximately 147021.9 kg.
It verifies bootstrap across both runs and persisted resume without counting the
inter-run gap. It is an aggregate plausibility regression, not an independent
replay of recorded signals; raw reference samples were not supplied.
Run these **read-only inventory commands** on the deployment host:
```sh
cd /opt/git-projects/production-analytics
sudo systemctl is-active production-analytics-bento1.service
sudo cat data/state/material/4d6cc209e35bc58887e5b4a3816835006425429e810e0d957f651971723e8c2f.json
sudo docker compose exec -T timescaledb psql -U production_analytics -d production_analytics -v ON_ERROR_STOP=1 <<'SQL'
BEGIN READ ONLY;
SELECT production_order, run_id, count(*) AS rows,
min(timestamp) AS first_snapshot, max(timestamp) AS last_snapshot,
min(consumption_kg), max(consumption_kg)
FROM material_consumption_snapshots
WHERE calculation_id = 'bento1-fresh-bentonite-consumption'
AND machine_id = '5f42a4f6-9ca0-4f6f-9786-40d50a35b230'
GROUP BY production_order, run_id ORDER BY first_snapshot;
SELECT * FROM material_consumption_snapshots
WHERE calculation_id = 'bento1-fresh-bentonite-consumption'
AND machine_id = '5f42a4f6-9ca0-4f6f-9786-40d50a35b230'
ORDER BY production_order, timestamp;
COMMIT;
SQL
```
After reviewing the inventory, and **only with cleanup authorization**, keep the
service stopped and execute this backup-and-delete transaction. The backup table
intentionally has no `IF NOT EXISTS`: a repeated invocation fails rather than
reusing an older backup. Only rows with the backed-up primary keys are deleted.
K7 rows are outside the predicate.
```sh
cd /opt/git-projects/production-analytics
sudo docker compose exec -T timescaledb psql -U production_analytics -d production_analytics -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
CREATE TABLE bento1_material_snapshots_before_rpm_20260907 AS
SELECT * FROM material_consumption_snapshots
WHERE calculation_id = 'bento1-fresh-bentonite-consumption'
AND machine_id = '5f42a4f6-9ca0-4f6f-9786-40d50a35b230';
DELETE FROM material_consumption_snapshots AS s
USING bento1_material_snapshots_before_rpm_20260907 AS old
WHERE s.calculation_id = old.calculation_id AND s.machine_id = old.machine_id
AND s.production_order = old.production_order AND s.timestamp = old.timestamp;
COMMIT;
SQL
sudo mv -n -- \
data/state/material/4d6cc209e35bc58887e5b4a3816835006425429e810e0d957f651971723e8c2f.json \
data/state/material/4d6cc209e35bc58887e5b4a3816835006425429e810e0d957f651971723e8c2f.json.before-rpm-20260907
```
Verify the original JSON path is absent before restarting; `mv -n` preserves an
existing backup and will not overwrite it. If inventory reveals other Bento
orders, derive their paths with `JsonMaterialStateStore._path(machine, order)`
and review any existing files before moving them too. Do not clear the shared
directory. Complete both state and snapshot cleanup before starting the new
code: the state schema deliberately does not detect a changed source model.
A later start bootstraps only the currently open order using its current ERP
width. It does not automatically rebuild snapshots for closed historical
orders. Reconstructing those requires a separately reviewed historical replay.