"""小时标签 + 前后 6 小时上下文 + 15 分钟槽位波形。""" from __future__ import annotations import json from datetime import timedelta from pathlib import Path import numpy as np import pandas as pd from . import config, data SLOT_MINUTES = 15 WAVEFORM_POINTS = 16 CONTEXT_HOURS = 6 COLORS = { "一缸": "#2980b9", "二缸": "#e67e22", "三缸": "#16a085", "四缸": "#8e44ad", "五缸": "#7f8c8d", "六缸": "#d35400", } def _slot_of(moment: pd.Timestamp, start: pd.Timestamp) -> int: return int((pd.Timestamp(moment) - start).total_seconds() // (SLOT_MINUTES * 60)) def _baseline_rms(meta: dict) -> float: return float(10 ** float(meta.get("rms_log", [0.0, 1.0])[0])) def export_context(frame: pd.DataFrame, intervals: pd.DataFrame, hourly_labels: pd.DataFrame, baseline: dict) -> None: frame = frame.copy() frame["sample_time"] = pd.to_datetime(frame["sample_time"]) frame = frame.dropna(subset=["cylinder"]).sort_values(["cylinder", "sample_time"]) if frame.empty or hourly_labels.empty: return global_start = frame["sample_time"].min().floor(f"{SLOT_MINUTES}min") global_end = frame["sample_time"].max().ceil(f"{SLOT_MINUTES}min") frame["slot"] = ((frame["sample_time"] - global_start).dt.total_seconds() // (SLOT_MINUTES * 60)).astype(int) frame = frame.drop_duplicates(["cylinder", "slot"], keep="first") frame_by_slot: dict[int, dict[str, pd.Series]] = {} for row in frame.itertuples(index=False): frame_by_slot.setdefault(int(row.slot), {})[row.cylinder] = row segments: list[dict] = [] needed_ids: set[int] = set() for item in hourly_labels.itertuples(index=False): context_start = pd.Timestamp(item.start_hour) - timedelta(hours=CONTEXT_HOURS) context_end = pd.Timestamp(item.end_hour) + timedelta(hours=CONTEXT_HOURS) context_start = max(context_start, global_start) context_end = min(context_end, global_end) context_start_slot = _slot_of(context_start, global_start) context_end_slot = _slot_of(context_end, global_start) slot_count = context_end_slot - context_start_slot + 1 label_start = _slot_of(item.start_hour, global_start) - context_start_slot label_end = _slot_of(item.end_hour, global_start) - context_start_slot blocks: dict[str, list[list[int]]] = {} for name in config.CYLINDERS: pieces = [] for local_slot in range(slot_count): row = frame_by_slot.get(context_start_slot + local_slot, {}).get(name) if row is None: continue file_id = int(row.id) def finite(value): return None if pd.isna(value) else round(float(value), 4) pieces.append([ local_slot, file_id, finite(row.score), finite(row.kurt), finite(row.kurt_ratio), finite(row.rms_ratio), ]) needed_ids.add(file_id) blocks[name] = pieces event_hits = [] for event in config.EVENTS: if event["cylinder"] != item.cylinder: continue center = pd.Timestamp(event["date"]) if pd.Timestamp(item.start_hour) <= center + timedelta(days=1) and pd.Timestamp(item.end_hour) >= center - timedelta(days=7): event_hits.append(event["event_id"]) segments.append( { "cylinder": item.cylinder, "event_hits": event_hits, "start_hour": str(item.start_hour), "end_hour": str(item.end_hour), "hour_count": int(item.hour_count), "direction": item.direction, "level": item.level, "confirmed": bool(item.confirmed), "max_score": float(item.max_score), "context_start": str(context_start), "slot_count": slot_count, "label_start": label_start, "label_end": label_end, "blocks": blocks, } ) print(f"[context] segments={len(segments)}, unique files={len(needed_ids)}", flush=True) samples = data.fetch_samples(sorted(needed_ids), chunk_size=200) waveforms: dict[int, list[list[float]]] = {} for file_id, signal in samples.items(): if signal is None or signal.size == 0: continue row = frame[frame["id"] == file_id] if row.empty: continue cylinder = row.iloc[0]["cylinder"] scale = _baseline_rms(baseline.get(cylinder, {})) if not np.isfinite(scale) or scale <= 0: scale = float(np.sqrt(np.mean(signal ** 2))) or 1.0 chosen = np.linspace(0, signal.size - 1, WAVEFORM_POINTS).astype(int) waveforms[file_id] = [ [round(i / (WAVEFORM_POINTS - 1), 4), round(float(signal[idx]) / scale, 4)] for i, idx in enumerate(chosen) ] print(f"[context] cached waveforms={len(waveforms)}", flush=True) payload = {"segments": segments, "waveforms": waveforms} out_dir = config.OUTPUT_DIR / "html" out_dir.mkdir(parents=True, exist_ok=True) html = """ 5号机小时标签

5号机小时标签

波形(各缸颜色,左轴) 异常分数(右轴) 实际峭度(右轴,虚线) 当前缸故障段 其他缸故障段 15 分钟槽位交替 上下文内其他标签
""" html = html.replace("__PAYLOAD__", json.dumps(payload, ensure_ascii=False)) (out_dir / "waveforms.html").write_text(html, encoding="utf-8") (out_dir / "index.html").write_text( """5号机缸体振动

5号机缸体振动

打开小时标签波形

""", encoding="utf-8", ) print(f"[html] {out_dir / 'waveforms.html'}")