edge-sentinel-classical / source /generate_data.py
ARotting's picture
Publish Device-held-out industrial anomaly classifier
572c4ce verified
Raw
History Blame Contribute Delete
4.74 kB
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
from datasets import Dataset
PROJECT_DIR = Path(__file__).resolve().parent
DATA_DIR = PROJECT_DIR / "data"
def build_device(device_id: int, steps: int, seed: int) -> pd.DataFrame:
rng = np.random.default_rng(seed + device_id)
time = np.arange(steps)
phase = device_id * 0.37
load = 0.55 + 0.27 * np.sin(time / 93 + phase) + 0.08 * np.sin(time / 17)
actuator = np.clip(48 + load * 42 + rng.normal(0, 1.5, steps), 0, 100)
temperature = 34 + load * 26 + rng.normal(0, 0.7, steps) + device_id * 0.12
pressure = 72 + load * 15 + rng.normal(0, 0.8, steps)
vibration = 0.8 + load**2 * 2.4 + rng.normal(0, 0.08, steps)
current = 5.2 + load * 13 + rng.normal(0, 0.25, steps)
flow = 18 + actuator * 0.42 + rng.normal(0, 0.6, steps)
packet_rate = 110 + load * 38 + rng.normal(0, 4, steps)
command_rate = 4 + np.abs(np.gradient(actuator)) * 0.38 + rng.normal(0, 0.2, steps)
labels = np.zeros(steps, dtype=np.int8)
anomaly_type = np.full(steps, "normal", dtype=object)
anomaly_names = [
"sensor_drift",
"actuator_mismatch",
"vibration_fault",
"pressure_spike",
"network_flood",
]
starts = rng.choice(np.arange(250, steps - 250), size=18, replace=False)
for index, start in enumerate(starts):
length = int(rng.integers(18, 55))
stop = min(start + length, steps)
kind = anomaly_names[index % len(anomaly_names)]
labels[start:stop] = 1
anomaly_type[start:stop] = kind
ramp = np.linspace(0, 1, stop - start)
if kind == "sensor_drift":
temperature[start:stop] += 8 * ramp
elif kind == "actuator_mismatch":
flow[start:stop] -= 12 + actuator[start:stop] * 0.12
elif kind == "vibration_fault":
vibration[start:stop] += 3.5 + rng.normal(0, 0.5, stop - start)
elif kind == "pressure_spike":
pressure[start:stop] += 18 * np.sin(np.linspace(0, np.pi, stop - start))
else:
packet_rate[start:stop] += 260 + rng.normal(0, 18, stop - start)
command_rate[start:stop] += 14
return pd.DataFrame(
{
"device_id": device_id,
"time": time,
"temperature": temperature,
"pressure": pressure,
"vibration": vibration,
"current": current,
"flow": flow,
"packet_rate": packet_rate,
"command_rate": command_rate,
"actuator_position": actuator,
"label": labels,
"anomaly_type": anomaly_type,
}
)
def add_features(frame: pd.DataFrame) -> pd.DataFrame:
sensors = [
"temperature",
"pressure",
"vibration",
"current",
"flow",
"packet_rate",
"command_rate",
"actuator_position",
]
groups = frame.groupby("device_id", sort=False)
for column in sensors:
frame[f"{column}_delta"] = groups[column].diff().fillna(0)
rolling = groups[column].rolling(24, min_periods=4)
mean = rolling.mean().reset_index(level=0, drop=True)
std = rolling.std().reset_index(level=0, drop=True).fillna(1).clip(lower=1e-3)
frame[f"{column}_z24"] = ((frame[column] - mean) / std).fillna(0)
frame["flow_actuator_residual"] = frame["flow"] - (
18 + frame["actuator_position"] * 0.42
)
frame["power_proxy"] = frame["current"] * frame["actuator_position"] / 100
return frame
def main() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
frame = pd.concat(
[build_device(device, steps=5000, seed=2026) for device in range(12)],
ignore_index=True,
)
frame = add_features(frame)
split = np.where(
frame["device_id"] <= 7,
"train",
np.where(frame["device_id"] <= 9, "validation", "test"),
)
frame["split"] = split
manifest = {}
for split_name in ["train", "validation", "test"]:
split_frame = frame[frame["split"] == split_name].reset_index(drop=True)
path = DATA_DIR / f"{split_name}.parquet"
Dataset.from_pandas(split_frame, preserve_index=False).to_parquet(path)
manifest[split_name] = {
"rows": len(split_frame),
"devices": sorted(split_frame["device_id"].unique().tolist()),
"anomaly_rate": float(split_frame["label"].mean()),
"path": path.name,
}
(DATA_DIR / "manifest.json").write_text(
json.dumps(manifest, indent=2),
encoding="utf-8",
)
print(json.dumps(manifest, indent=2))
if __name__ == "__main__":
main()