| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- """Sample and rank 4/6 unit valve events without scanning wave_sample_one."""
- from __future__ import annotations
- from collections import defaultdict
- from datetime import datetime, timedelta
- import numpy as np
- from extract_from_db import fetch_cycles, fetch_file_rows, point_name, sample_per_day
- from indicator_features.indicator_features import CYLINDER_BORE_MM, _plateau_levels, smooth, volume_from_angle
- CYLINDERS = ("一", "二", "三", "四", "五", "六")
- SIDES = ("盖侧", "轴侧")
- BAD = {
- "4": {("二", "盖侧"), ("四", "盖侧"), ("六", "盖侧")},
- "6": {("二", "盖侧"), ("四", "盖侧"), ("六", "盖侧")},
- }
- EVENTS = [
- {
- "key": "4号 2026-06-10 气阀事件",
- "unit": "4", "center": "2026-06-10", "pre_end": "2026-06-10", "post_start": "2026-06-10",
- "start": "2026-05-11", "end": "2026-07-11", "markers": ["2026-06-10"],
- },
- {
- "key": "6号 2026-04-22~04-26 气阀维修过程",
- "unit": "6", "center": "2026-04-24", "pre_end": "2026-04-22", "post_start": "2026-04-26",
- "start": "2026-03-23", "end": "2026-05-27", "markers": ["2026-04-22", "2026-04-26"],
- },
- ]
- def _median(items, key):
- return np.median([item[key] for item in items], axis=0).round(6).tolist() if items else []
- def collect_events(connection, per_day=6, bins=90):
- result = {}
- for config in EVENTS:
- center = datetime.fromisoformat(config["center"])
- pre_end = datetime.fromisoformat(config["pre_end"])
- post_start = datetime.fromisoformat(config["post_start"])
- points = {}
- for cylinder in CYLINDERS:
- for side in SIDES:
- if (cylinder, side) in BAD[config["unit"]]:
- continue
- files = fetch_file_rows(
- connection, point_name(config["unit"], cylinder, side), config["start"], config["end"]
- )
- picked = sample_per_day(files, per_day)
- cycles = fetch_cycles(connection, [int(row["id"]) for row in picked], 100) if picked else {}
- samples = []
- for row in picked:
- signal = cycles.get(int(row["id"]))
- if signal is None or len(signal) < 60 or not np.all(np.isfinite(signal)):
- continue
- pressure = smooth(signal, 21)
- suction, discharge, _ = _plateau_levels(pressure)
- span = discharge - suction
- if not np.isfinite(span) or span < .3:
- continue
- indices = np.arange(bins) * len(pressure) / bins
- absolute = np.interp(indices, np.arange(len(pressure)), pressure)
- profile = (absolute - suction) / span
- volume = np.interp(
- indices, np.arange(len(pressure)),
- volume_from_angle(len(pressure), CYLINDER_BORE_MM[cylinder]),
- )
- time = row["sample_time"]
- if not isinstance(time, datetime):
- time = datetime.fromisoformat(str(time))
- if time < pre_end:
- phase = "修前"
- elif time >= post_start:
- phase = "修后"
- else:
- phase = "过程内"
- samples.append({
- "date": time.strftime("%Y-%m-%d %H:%M:%S"),
- "day": (time.date() - center.date()).days,
- "phase": phase,
- "profile": profile.round(6).tolist(),
- "absolute": absolute.round(6).tolist(),
- "volume": volume.round(6).tolist(),
- })
- samples.sort(key=lambda sample: sample["date"])
- pre = [sample for sample in samples if sample["phase"] == "修前"]
- post = [sample for sample in samples if sample["phase"] == "修后"]
- if not pre or not post:
- continue
- before = np.median([sample["profile"] for sample in pre], axis=0)
- after = np.median([sample["profile"] for sample in post], axis=0)
- points[(cylinder, side)] = {
- "samples": samples, "pre": pre, "post": post,
- "before": before, "after": after, "delta": after - before,
- }
- common = np.median([point["delta"] for point in points.values()], axis=0)
- cylinder_scores = defaultdict(list)
- for (cylinder, _), point in points.items():
- point["total"] = float(np.linalg.norm(point["delta"]))
- point["residual"] = float(np.linalg.norm(point["delta"] - common))
- cylinder_scores[cylinder].append(point["residual"])
- ranking = sorted(
- ((cylinder, max(scores)) for cylinder, scores in cylinder_scores.items()),
- key=lambda item: item[1], reverse=True,
- )
- event = {
- "label": config["key"], "unit": config["unit"], "markers": config["markers"],
- "ranking": [{"cylinder": cylinder, "residual": round(score, 5)} for cylinder, score in ranking],
- "sides": {},
- }
- for (cylinder, side), point in points.items():
- template = point["before"]
- for sample in point["samples"]:
- sample["distance"] = round(float(np.linalg.norm(np.asarray(sample["profile"]) - template)), 5)
- name = f"{cylinder}缸{side}"
- event["sides"][name] = {
- "samples": point["samples"],
- "counts": {"pre": len(point["pre"]), "post": len(point["post"])},
- "preMedian": point["before"].round(6).tolist(),
- "postMedian": point["after"].round(6).tolist(),
- "preAbsoluteMedian": _median(point["pre"], "absolute"),
- "postAbsoluteMedian": _median(point["post"], "absolute"),
- "trend": [{"date": sample["date"], "day": sample["day"], "distance": sample["distance"]}
- for sample in point["samples"]],
- "totalChange": round(point["total"], 5),
- "localResidual": round(point["residual"], 5),
- "markerDays": [
- (datetime.fromisoformat(marker).date() - center.date()).days
- for marker in config["markers"]
- ],
- }
- coverage = sorted({sample["date"][:10] for point in event["sides"].values() for sample in point["samples"]})
- event["label"] += f" · 实际功图 {coverage[0]} 至 {coverage[-1]}"
- result[config["key"]] = event
- return result
- def top_cylinder_event(event, limit=3):
- cylinders = {item["cylinder"] for item in event["ranking"][:limit]}
- selected = dict(event)
- selected["sides"] = {
- name: point for name, point in event["sides"].items()
- if name[0] in cylinders
- }
- return selected
|