Skip to content

Repository files navigation

stream-anomaly-sentinel

Streaming anomaly detection for semiconductor equipment telemetry with exactly-once alert output: 100% recall on four labeled fault classes, 1.7 s mean spike detection latency, byte-identical replay.

CI coverage license replay

What this solves

  • Batch anomaly reports on fab tool telemetry arrive hours after the excursion; this pipeline alerts in stream time, seconds to minutes from onset.
  • Restarts and redeliveries normally mean duplicate or missing alerts; here every event affects the output exactly once, provably.
  • Post-incident analysis needs trustworthy reprocessing; any offset range replays to byte-identical alerts.

Executive summary

Unplanned downtime on a single semiconductor process tool is commonly costed at around $100k per hour, and the excursions that cause it (a stuck pressure reading, a slow calibration drift, RF arcing, a heater loop that stops tracking power) are visible in sensor telemetry well before scrap accumulates. The operational failure is latency: when anomaly detection runs as a nightly batch job, the report describes yesterday's problem. The window in which an engineer could have acted is already closed.

This project is a streaming pipeline that closes that window mechanically rather than heroically. A seeded simulator emits multi-tool, multi-channel telemetry with labeled fault injections. Events flow through a broker abstraction (SQLite log locally, Kafka client for real deployments) into a processor running four sliding-window detectors: robust z-score on first differences (median/MAD, spike and step faults), flatline run-length (stuck sensors), dual-EWMA control bands (slow drift), and rolling Pearson correlation across physically coupled channels (faults invisible to any single channel). The twist is exactly-once output on top of an at-least-once broker: alert batches commit atomically with the offset-plus-state checkpoint in one SQLite transaction, alert ids are deterministic content hashes, and the checkpointed offset acts as a dedupe watermark that drops redelivered events before they can touch warm detector state.

Measured on the committed labeled benchmark (24 injected faults, 115,200 events, seed 42): 100% recall on all four fault classes with 94.7% alert precision; mean detection latency of 1.7 s for spike bursts, 29.4 s for stuck sensors, 194 s for correlation breaks, and 206 s for slow drift (a drift is by definition small early). Sustained throughput is 15,738 events/sec at the default window (18,019 at window 64) with p99 per-event processing latency of 241 microseconds, on 2 shared vCPUs. Replaying the full log and a 60,001-event segment twice each produced byte-identical alert files (sha256 verified, zero duplicate alert ids), and the live run hash equals the replay hash.

Architecture

Labeled diagram: solid arrows are the data path, annotations mark failure boundaries.

flowchart LR
    subgraph SIM["Simulator (seeded, labeled)"]
        T1["Tools ETCH-01..CVD-02<br/>temperature, rf_power,<br/>pressure, vibration"]
    end
    subgraph BROKER["Broker (at-least-once boundary)"]
        LOG["Append-only log<br/>LocalBroker: SQLite WAL<br/>KafkaBroker: Redpanda/Kafka"]
    end
    subgraph PROC["Processor (crash boundary)"]
        DET["Detector banks per tool/channel<br/>robust_z, flatline, ewma_drift"]
        CORR["corr_break per tool<br/>(temperature+rf_power)"]
        CP["Checkpoint: offset + state<br/>dedupe watermark"]
    end
    subgraph SINK["Alert store (exactly-once boundary)"]
        DB["SQLite: alerts + checkpoints<br/>ONE transaction"]
        JSONL["canonical alerts.jsonl<br/>sha256-stable"]
    end
    RCLI["Replay CLI<br/>read_range, fresh state"]
    T1 -->|produce| LOG
    LOG -->|"consume (position != committed)"| DET
    LOG --> CORR
    DET --> CP
    CORR --> CP
    CP -->|atomic commit| DB
    DB --> JSONL
    LOG -.->|"offset range"| RCLI
    RCLI -.->|"byte-identical output"| JSONL
Loading

Fault labels flow only to the evaluator, never to the processor.

Tech stack

Component Choice Why here specifically
Language Python 3.10+ Detector math is windowed scalar work; stdlib deque + sqlite3 cover the hot path without a framework
Validation pydantic 2 Config errors surface at startup with field-level messages, not mid-stream
Broker (executed) SQLite WAL log Same contract as one Kafka partition (offsets, commits, ranged reads) with zero infra; what all committed numbers ran on
Broker (deployment) kafka-python + Redpanda compose Same Broker interface; single-partition topics preserve the total order replay relies on
State/alerts SQLite, one file Alert batch + checkpoint commit in one transaction; the exactly-once mechanism (ADR-002)
Numerics numpy (simulator only) Vectorized noise generation; detectors are dependency-free on purpose
Tests pytest + pytest-cov 56 tests, 95% coverage, including crash-recovery and redelivery regressions
Lint/CI ruff + GitHub Actions CI re-proves replay determinism on every push, not just tests

Author-built: simulator, broker abstraction and both implementations, all four detectors, checkpoint/watermark processor, replay and evaluation tooling, benchmark harness, SVG chart generator. Library-provided: numpy RNG, pydantic validation, sqlite3, kafka-python client.

Quickstart

git clone https://github.com/panchalvedant13/stream-anomaly-sentinel.git
cd stream-anomaly-sentinel
python3 -m venv venv && . venv/bin/activate
pip install -e .[dev]

# 1. generate the labeled telemetry stream (deterministic, seed 42)
sentinel simulate                      # 115,200 events, 24 labeled faults

# 2. run the streaming processor
sentinel process                       # prints alert count + sha256

# 3. score alerts against ground truth
sentinel evaluate                      # per-fault recall, latency, precision

# 4. prove idempotency: two replays, identical bytes
sentinel replay --out data/replay --name pass1
sentinel replay --out data/replay --name pass2
sha256sum data/replay/pass1.alerts.jsonl data/replay/pass2.alerts.jsonl

The two sha256 lines must be identical, and both must equal the alerts_sha256 printed by step 2. Determinism is guaranteed within a platform (same CPU float behavior and libm); CI therefore proves replay identity on its own runner rather than pinning an absolute hash. Tests: pytest --cov=src/sentinel.

Performance under load

Methodology: drain the full committed benchmark topic (115,200 events, 16 tool/channel keys) through a fresh processor per configuration, single process, LocalBroker on SQLite. Wall time gives sustained events/sec; perf_counter_ns around each parse-detect-buffer step gives per-event latency, with the inline checkpoint commit every 500 events landing in the tail. Environment: 2 vCPU, 4GB shared container. Raw output: benchmark/results/throughput.json. Reproduce with sentinel bench.

Throughput and latency vs window size

Chart: measured throughput (left) and p50/p95/p99 processing latency (right) per window size.

config events/sec p50 (us) p95 (us) p99 (us) max (us)
window 64 18,019 17.5 106.4 252.4 43,557
window 128 (default) 15,738 16.3 118.2 241.2 445,625
window 256 10,141 15.0 174.6 313.0 389,759

Honest degradation: throughput drops 44% from window 64 to 256 because MAD recomputation is O(n log n) in window size, and the worst-case single-event stall is the inline SQLite checkpoint commit at up to ~0.45 s; a deployment that cannot tolerate that pause should move the commit off the hot path.

Detection quality on the same run (benchmark/results/detection_quality.json):

fault type faults detected recall mean latency (s) max latency (s)
spike 6 6 1.00 1.7 6.7
stuck 6 6 1.00 29.4 29.8
corr_break 6 6 1.00 194.2 277.1
drift 6 6 1.00 205.8 250.0

Alert precision: flatline 1.00, corr_break 1.00, robust_z 0.93, ewma_drift 0.90; overall 94.7% (54 of 57 alerts matched a labeled fault; the 3 false positives are EWMA baseline re-convergence firing minutes after a large drift cleared, plus one marginal z=5.68 tail crossing).

Architecture decisions

Full records in docs/adr/:

  • ADR-001: robust statistics (median/MAD, EWMA) over learned autoencoder detectors, for this latency and explainability profile.
  • ADR-002: SQLite checkpoint and alert store over Redis/Postgres for a single-node agent, with the trigger to move and the Kafka path for scale.

Intentionally out of scope

  • No multivariate learned models until the robust detectors miss a documented fault class (recall under 90% with tuned thresholds on real telemetry). Today they would add opacity, not recall.
  • No Flink/Spark until sustained input exceeds the single-node ceiling (~15k events/sec measured here; a 200-tool fab at 4 channels x 1 Hz is 800 events/sec).
  • No multi-partition ordering: topics are single-partition per tool group by design; scale out is more agents, not a shuffle.
  • No alert routing/paging integration; the store and JSONL export are the integration surface.
  • No online threshold auto-tuning; thresholds are config, changed deliberately and versioned.

Security and compliance

  • No secrets in code or config files; broker credentials for the Kafka path come from environment variables only (SENTINEL_* overrides follow the same rule).
  • Logs are structured JSON and carry aggregates only (counts, offsets, latencies); raw telemetry values never appear in logs, so log shipping cannot leak process data.
  • OT/IT separation: the agent is designed to run on the OT side next to the tool data source; only the alert store contents (alert metadata, no raw traces) need to cross to IT, and the SQLite file makes that a one-way file transfer rather than an open port.
  • Input validation at every boundary: config through pydantic, events re-parsed and type-checked, poisoned messages quarantined by offset without payload logging.

Failure modes

Failure Detection Behavior Recovery
Broker outage consume raises / returns nothing processor exits current batch cleanly; checkpoint is durable restart when broker returns; resumes at watermark, no loss, no duplicates
Poisoned message Event parse fails counted, logged with offset only, skipped; partition never stalls inspect quarantined offsets via replay --from-offset N
Checkpoint corruption typed CheckpointCorrupt on load refuses to start with silent state loss delete checkpoint row, replay from a known-good offset; deterministic alert ids absorb duplicates
Crash mid-batch uncommitted work lost with its checkpoint (atomic) at-least-once redelivery on restart watermark skips already-committed offsets; regenerated alerts dedupe by id (tested)
Clock skew / late events event time (ts) is carried in payload, processing order is offset order detectors key on stream time, cooldowns use event ts a late event replays into the same deterministic alert id; wall clock is never load-bearing

Hardest problem solved

The replay proof kept passing while the numbers were quietly wrong. On the first full run the processor reported 20,000 events processed from a 19,200-event topic. Nothing crashed; the alert hashes even matched. The local broker was serving consume() from the group's committed offset, and since commits happened every 500 events, the uncommitted tail of every batch was silently redelivered and re-applied to warm detector windows. Fixed by giving brokers a real consumer position separate from the committed offset, Kafka-poll style (08b5074). Chasing the fallout exposed a nastier cousin: after every stuck-sensor fault, the diff window was full of exact zeros, the MAD collapsed as healthy data returned, and the first normal readings scored z > 2000, firing confident false spike alerts 65 seconds after each stuck fault cleared. The fix is a zero-fraction guard: no scoring while more than 35% of the window is exactly zero, because that regime belongs to the flatline detector (79c4339, regression-tested in test_robust_z_no_blowup_after_flatline).

Future work

  • Kafka integration test job in CI using the committed Redpanda compose file (service containers), so the KafkaBroker path gets executed proof, not just interface parity.
  • Async alert export and off-hot-path checkpointing to cut the ~0.45 s worst-case commit stall.
  • Per-channel threshold commissioning helper: fit MAD/EWMA bands from a labeled healthy window and emit a config patch.
  • Cross-tool fleet detector: the same excursion signature appearing on sibling chambers within a time window is a different, higher-severity event.
  • Prometheus metrics endpoint (counters and latency histograms already exist internally).

License

MIT. This is a portfolio project; the telemetry is simulated and labeled, and every number above is reproducible from the committed code and seed.

About

Streaming anomaly detection for semiconductor fab telemetry. Robust windowed detectors (median/MAD, EWMA, correlation-break) with exactly-once alert output via transactional checkpoints and dedupe watermarks. 100% recall on 4 labeled fault classes, 15.7k events/sec on 2 vCPUs, byte-identical replay proven by sha256.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages