Initial commit with Dockerfile and demo code
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Per-signal anomaly detection against a learned baseline.
|
||||
*
|
||||
* The baseline is learned from a warmup period of clean running and then FROZEN.
|
||||
* That freeze is the important design choice: a continuously adapting baseline
|
||||
* quietly absorbs a slow ramp, so the exact failure mode this demo is built to
|
||||
* catch would never raise a z-score. Freezing means "different from how this
|
||||
* machine normally behaves", which is what an operator actually wants to know.
|
||||
*/
|
||||
|
||||
export class BaselineTracker {
|
||||
/**
|
||||
* warmupSec - simulated seconds of clean data used to learn mean and spread
|
||||
* minStd - floor on the standard deviation, so a very quiet signal does not
|
||||
* produce enormous z-scores from rounding-level noise
|
||||
*/
|
||||
constructor({ warmupSec = 300, minStd = 1e-3 } = {}) {
|
||||
this.warmupSec = warmupSec;
|
||||
this.minStd = minStd;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.n = 0;
|
||||
this.sum = 0;
|
||||
this.sumSq = 0;
|
||||
this.mean = 0;
|
||||
this.std = 0;
|
||||
this.ready = false;
|
||||
this.startT = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a sample. `clean` should be false when the line is in a known abnormal
|
||||
* state, so the baseline never learns from a fault it is supposed to detect.
|
||||
*/
|
||||
update(t, value, clean = true) {
|
||||
if (!Number.isFinite(value)) return;
|
||||
if (this.startT === null) this.startT = t;
|
||||
|
||||
if (!this.ready) {
|
||||
if (clean) {
|
||||
this.n += 1;
|
||||
this.sum += value;
|
||||
this.sumSq += value * value;
|
||||
}
|
||||
if (t - this.startT >= this.warmupSec && this.n > 20) {
|
||||
this.mean = this.sum / this.n;
|
||||
const variance = Math.max(0, this.sumSq / this.n - this.mean * this.mean);
|
||||
this.std = Math.max(this.minStd, Math.sqrt(variance));
|
||||
this.ready = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Signed z-score, or null while the baseline is still being learned. */
|
||||
z(value) {
|
||||
if (!this.ready || !Number.isFinite(value)) return null;
|
||||
return (value - this.mean) / this.std;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A bank of baselines keyed by "STATION.signal".
|
||||
*
|
||||
* The exclusion list is DERIVED from the asset model rather than hardcoded here,
|
||||
* so when a signal is added to server/sim/stations.js the decision about whether
|
||||
* it can be anomaly-tested lives next to its definition. Two kinds are excluded:
|
||||
*
|
||||
* cumulative - monotonically accumulating values (tool wear, part counters).
|
||||
* Normal operation carries them far from any frozen baseline, so a
|
||||
* z-score reports ordinary accumulation as a fault. Tool wear
|
||||
* reaching 27% against a learned 18% baseline is not an anomaly,
|
||||
* it is a Tuesday.
|
||||
* volatile - values that legitimately swing with station state (belt speed
|
||||
* goes to zero on every micro-stop) or that are operator inputs
|
||||
* rather than measurements (a setpoint).
|
||||
*
|
||||
* Both remain fully covered by threshold alarms and trend projection, which are
|
||||
* the appropriate detectors for them.
|
||||
*/
|
||||
import { STATION_SPECS } from '../sim/stations.js';
|
||||
|
||||
const EXCLUDED = new Set(
|
||||
STATION_SPECS.flatMap((spec) =>
|
||||
spec.signals
|
||||
.filter((g) => g.cumulative || g.volatile)
|
||||
.map((g) => `${spec.id}.${g.key}`),
|
||||
),
|
||||
);
|
||||
|
||||
export class BaselineBank {
|
||||
constructor(opts = {}) {
|
||||
this.opts = opts;
|
||||
this.trackers = new Map();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.trackers.clear();
|
||||
}
|
||||
|
||||
key(stationId, signal) {
|
||||
return `${stationId}.${signal}`;
|
||||
}
|
||||
|
||||
tracked(stationId, signal) {
|
||||
return !EXCLUDED.has(this.key(stationId, signal));
|
||||
}
|
||||
|
||||
get(stationId, signal) {
|
||||
const k = this.key(stationId, signal);
|
||||
let tr = this.trackers.get(k);
|
||||
if (!tr) {
|
||||
tr = new BaselineTracker(this.opts);
|
||||
this.trackers.set(k, tr);
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
|
||||
update(stationId, signal, t, value, clean) {
|
||||
if (!this.tracked(stationId, signal)) return;
|
||||
this.get(stationId, signal).update(t, value, clean);
|
||||
}
|
||||
|
||||
z(stationId, signal, value) {
|
||||
if (!this.tracked(stationId, signal)) return null;
|
||||
return this.get(stationId, signal).z(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user