Document real-world peak detector validation
This commit is contained in:
+21
-5
@@ -26,7 +26,8 @@ after a value falls below `drop_ratio * current_peak`, it emits that maximum
|
|||||||
only after actual subsequent below-threshold observations support a continuous
|
only after actual subsequent below-threshold observations support a continuous
|
||||||
`hold_seconds` interval. `min_peak` is a generic detector setting, along with
|
`hold_seconds` interval. `min_peak` is a generic detector setting, along with
|
||||||
`drop_ratio` (0 < ratio < 1), `hold_seconds` (>= 0), and an explicit positive
|
`drop_ratio` (0 < ratio < 1), `hold_seconds` (>= 0), and an explicit positive
|
||||||
`max_sample_gap_seconds`. Elapsed time, never a sample count, determines
|
`max_sample_gap_seconds`. They are selected per signal, rather than being
|
||||||
|
globally fixed process constants. Elapsed time, never a sample count, determines
|
||||||
confirmation. An interval longer than the configured maximum sample gap
|
confirmation. An interval longer than the configured maximum sample gap
|
||||||
restarts a reset candidate, so a timestamp gap is not
|
restarts a reset candidate, so a timestamp gap is not
|
||||||
continuous-below evidence. Reset values can be negative rather than zero,
|
continuous-below evidence. Reset values can be negative rather than zero,
|
||||||
@@ -35,10 +36,25 @@ The maximum gap is required rather than inferred or defaulted, making each
|
|||||||
source's continuity assumption explicit.
|
source's continuity assumption explicit.
|
||||||
|
|
||||||
K7 roll length, variable `bf81c547-dccf-4709-aee2-0f78366d1dfc` (m), is the
|
K7 roll length, variable `bf81c547-dccf-4709-aee2-0f78366d1dfc` (m), is the
|
||||||
first real-world validation case. The tested capture is continuous on a
|
clean reference validation case. With `min_peak=10`, `drop_ratio=0.75`,
|
||||||
10-second grid through its latest available sample; its shorter-than-requested
|
`hold_seconds=60`, and `max_sample_gap_seconds=20`, its continuous 10-second
|
||||||
result was caused by an end time queried in the future, not observed time-series
|
capture produced exactly four plausible peaks of approximately 49.98 m. Its
|
||||||
gaps. The raw capture remains ignored and deterministic tests use synthetic
|
shorter-than-requested result was caused by an end time queried in the future,
|
||||||
|
not observed time-series gaps.
|
||||||
|
|
||||||
|
Bento 2 roll weight is the second, more realistic robustness validation case:
|
||||||
|
machine `6201f3de-b940-45d7-930c-1481c5ae9e1d`, variable
|
||||||
|
`059c0015-9aff-4270-a4ce-f21b83f99378` (kg). Its observed cadence is 10
|
||||||
|
seconds, but it has long plateaus, reset values roughly 300–470 kg, and
|
||||||
|
materially varying peaks. With `min_peak=800`, `drop_ratio=0.75`,
|
||||||
|
`hold_seconds=60`, and `max_sample_gap_seconds=20`, the extended capture
|
||||||
|
yielded 13 plausible completed peaks: 936, 919, 935, 930, 930, 920, 909,
|
||||||
|
1195, 921, 930, 939, 912, and 914 kg (from 00:46:50Z through 03:06:30Z on
|
||||||
|
2026-09-04). The approximately 1195 kg peak is retained as a real
|
||||||
|
process/mechanical condition, not special-cased as a defect; it is emitted
|
||||||
|
only after its following reset has enough below-threshold observations. A
|
||||||
|
reset is not expected to approach zero, and peak values are not assumed to be
|
||||||
|
constant. Raw captures remain ignored and deterministic tests use synthetic
|
||||||
fixtures only.
|
fixtures only.
|
||||||
|
|
||||||
## Central domain context
|
## Central domain context
|
||||||
|
|||||||
@@ -69,20 +69,34 @@ roll weight, and comparable sawtooth/batch signals. It retains the current
|
|||||||
maximum and emits it only after the value is below
|
maximum and emits it only after the value is below
|
||||||
`drop_ratio * current_peak` continuously for `hold_seconds`. Configuration is
|
`drop_ratio * current_peak` continuously for `hold_seconds`. Configuration is
|
||||||
`min_peak`, `drop_ratio` (strictly between 0 and 1), non-negative
|
`min_peak`, `drop_ratio` (strictly between 0 and 1), non-negative
|
||||||
`hold_seconds`, and positive `max_sample_gap_seconds`. The hold uses elapsed
|
`hold_seconds`, and positive `max_sample_gap_seconds`. These are per-signal
|
||||||
|
configuration parameters, not globally fixed process constants. The hold uses elapsed
|
||||||
timestamps, never a sample count; an interval longer than the configured
|
timestamps, never a sample count; an interval longer than the configured
|
||||||
maximum sample gap restarts a reset candidate,
|
maximum sample gap restarts a reset candidate,
|
||||||
so a timestamp gap alone is not evidence that a signal remained below threshold.
|
so a timestamp gap alone is not evidence that a signal remained below threshold.
|
||||||
`max_sample_gap_seconds` is required rather than defaulted, so the source's
|
`max_sample_gap_seconds` is required rather than defaulted, so the source's
|
||||||
continuity assumption is explicit for each detector configuration.
|
continuity assumption is explicit for each detector configuration.
|
||||||
Reset values can be negative and need not be zero. Equal timestamps are
|
Reset values need not approach zero (and can be negative), and peak magnitude
|
||||||
accepted in arrival order but add no elapsed hold time; backwards timestamps
|
can vary materially between cycles. Equal timestamps are accepted in arrival
|
||||||
are rejected.
|
order but add no elapsed hold time; backwards timestamps are rejected.
|
||||||
|
|
||||||
The first real-world validation case is K7 roll length (m), variable
|
Real-world validations use ignored local captures and are not part of the test
|
||||||
`bf81c547-dccf-4709-aee2-0f78366d1dfc`. When the ignored local capture is
|
suite. K7 roll length (m), variable `bf81c547-dccf-4709-aee2-0f78366d1dfc`, is
|
||||||
present, run `python scripts/validate_k7_roll_length.py` to inspect detected
|
the clean reference case: its 10-second capture produced exactly four plausible
|
||||||
peaks without adding the raw capture to tests or version control.
|
peaks of about 49.98 m with `min_peak=10`, `drop_ratio=0.75`,
|
||||||
|
`hold_seconds=60`, and `max_sample_gap_seconds=20`. Run
|
||||||
|
`python scripts/validate_k7_roll_length.py` when that capture is available.
|
||||||
|
|
||||||
|
Bento 2 roll weight (kg), machine `6201f3de-b940-45d7-930c-1481c5ae9e1d`,
|
||||||
|
variable `059c0015-9aff-4270-a4ce-f21b83f99378`, is the more realistic
|
||||||
|
robustness case. Its observed 10-second signal has plateaus, reset values of
|
||||||
|
roughly 300–470 kg, and materially varying peak heights. With
|
||||||
|
`min_peak=800`, `drop_ratio=0.75`, `hold_seconds=60`, and
|
||||||
|
`max_sample_gap_seconds=20`, the extended capture produces 13 plausible
|
||||||
|
completed peaks, including a legitimate approximately 1195 kg cycle. Run
|
||||||
|
`python scripts/validate_bento2_roll_weight.py` when the ignored capture is
|
||||||
|
available. Neither capture should be added to version control or automated
|
||||||
|
tests.
|
||||||
|
|
||||||
`docker compose up -d timescaledb` is an optional local database design for a
|
`docker compose up -d timescaledb` is an optional local database design for a
|
||||||
future persistence milestone. It is not required for the bootstrap tests.
|
future persistence milestone. It is not required for the bootstrap tests.
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ This is calculation-domain logic, with no ENLYZE transport dependency.
|
|||||||
The detector uses timestamps rather than a sample count. An interval longer
|
The detector uses timestamps rather than a sample count. An interval longer
|
||||||
than the configured positive `max_sample_gap_seconds` restarts a reset candidate,
|
than the configured positive `max_sample_gap_seconds` restarts a reset candidate,
|
||||||
so a timestamp alone does not establish that a signal was continuously below
|
so a timestamp alone does not establish that a signal was continuously below
|
||||||
threshold. Equal
|
threshold. These parameters (`min_peak`, `drop_ratio`, `hold_seconds`, and
|
||||||
timestamps are processed in arrival order but contribute no elapsed time, and
|
`max_sample_gap_seconds`) are selected per signal rather than treated as global
|
||||||
backwards timestamps are rejected. Reset values are not assumed to be zero.
|
process constants. Equal timestamps are processed in arrival order but
|
||||||
|
contribute no elapsed time, and backwards timestamps are rejected. Reset values
|
||||||
|
are not assumed to approach zero, and peak values may vary materially between
|
||||||
|
cycles.
|
||||||
|
|||||||
+4
-1
@@ -31,7 +31,10 @@ tests against sanitized fixtures.
|
|||||||
The generic configurable sawtooth detector is implemented: it retains the
|
The generic configurable sawtooth detector is implemented: it retains the
|
||||||
current maximum, closes a cycle after a configurable below-peak fraction has
|
current maximum, closes a cycle after a configurable below-peak fraction has
|
||||||
actual sample support for a configurable elapsed duration, and emits one
|
actual sample support for a configurable elapsed duration, and emits one
|
||||||
completed peak. Its pure state can later be persisted for incremental use.
|
completed peak. Its pure state can later be persisted for incremental use. It
|
||||||
|
has been validated with a clean K7 roll-length reference signal and a more
|
||||||
|
variable Bento 2 roll-weight robustness signal; the latter includes plateaus,
|
||||||
|
non-zero resets, and varying peak heights without detector special cases.
|
||||||
|
|
||||||
## Later
|
## Later
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate the peak detector against an ignored local Bento 2 capture.
|
||||||
|
|
||||||
|
This developer utility reads no network data and does not modify the capture.
|
||||||
|
Run it from the repository root when
|
||||||
|
data/raw/enlyze/bento2-roll-weight-extended.raw.json is available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from production_analytics.calculations import (
|
||||||
|
PeakCycleDetector,
|
||||||
|
PeakDetectorConfig,
|
||||||
|
TimeSeriesSample,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CAPTURE = Path("data/raw/enlyze/bento2-roll-weight-extended.raw.json")
|
||||||
|
VARIABLE_UUID = "059c0015-9aff-4270-a4ce-f21b83f99378"
|
||||||
|
EXPECTED_PEAKS = (
|
||||||
|
("2026-09-04T00:46:50+00:00", 936.0),
|
||||||
|
("2026-09-04T00:57:10+00:00", 919.0),
|
||||||
|
("2026-09-04T01:07:20+00:00", 935.0),
|
||||||
|
("2026-09-04T01:17:20+00:00", 930.0),
|
||||||
|
("2026-09-04T01:27:20+00:00", 930.0),
|
||||||
|
("2026-09-04T01:37:30+00:00", 920.0),
|
||||||
|
("2026-09-04T01:48:00+00:00", 909.0),
|
||||||
|
("2026-09-04T02:14:20+00:00", 1195.0),
|
||||||
|
("2026-09-04T02:24:10+00:00", 921.0),
|
||||||
|
("2026-09-04T02:35:40+00:00", 930.0),
|
||||||
|
("2026-09-04T02:46:10+00:00", 939.0),
|
||||||
|
("2026-09-04T02:56:20+00:00", 912.0),
|
||||||
|
("2026-09-04T03:06:30+00:00", 914.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not CAPTURE.is_file():
|
||||||
|
print(f"Bento 2 capture is not available: {CAPTURE}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
payload = json.loads(CAPTURE.read_text(encoding="utf-8"))
|
||||||
|
data = payload["response"]["body"]["data"]
|
||||||
|
value_index = data["columns"].index(VARIABLE_UUID)
|
||||||
|
samples = (
|
||||||
|
TimeSeriesSample(
|
||||||
|
datetime.fromisoformat(record[0].replace("Z", "+00:00")),
|
||||||
|
float(record[value_index]),
|
||||||
|
)
|
||||||
|
for record in data["records"]
|
||||||
|
if record[value_index] is not None
|
||||||
|
)
|
||||||
|
detector = PeakCycleDetector(
|
||||||
|
PeakDetectorConfig(
|
||||||
|
min_peak=800.0,
|
||||||
|
drop_ratio=0.75,
|
||||||
|
hold_seconds=60.0,
|
||||||
|
max_sample_gap_seconds=20.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
peaks = list(detector.process_many(samples))
|
||||||
|
observed = tuple((peak.peak_timestamp.isoformat(), peak.value) for peak in peaks)
|
||||||
|
|
||||||
|
print(f"Detected peaks: {len(peaks)}")
|
||||||
|
for peak in peaks:
|
||||||
|
print(
|
||||||
|
f"peak={peak.value:.0f} kg at {peak.peak_timestamp.isoformat()} "
|
||||||
|
f"confirmed={peak.confirmed_at.isoformat()}"
|
||||||
|
)
|
||||||
|
if observed != EXPECTED_PEAKS:
|
||||||
|
print("Validation failed: detected peaks differ from the expected extended-window result.")
|
||||||
|
return 2
|
||||||
|
print("Validation passed: detected peaks match the 13 expected completed peaks.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user