Переглянути джерело

调整振动阶段评分权重

18922397810 2 тижнів тому
батько
коміт
53fb23dc21

+ 34 - 7
PKS润滑油压力与振动算法说明.md

@@ -401,13 +401,13 @@ python backend/scan_pks_fault_evolution.py \
 
 优先看以下字段:
 
-- `振动等级`;
-- `主要振动侧`;
-- `最大振动值`;
-- `触发小时数`;
-- `严重信号小时数`;
-- `最大峰值相对近期基线偏离`;
-- `变化形态`。
+- `振动阶段`;
+- `阶段综合评分`及各项分项得分;
+- `最强异常侧`;
+- `阶段最大振动值`;
+- `最强侧近期峰值基线`和`最强侧近期稳健尺度`;
+- `阶段最大峰值/基线比例`;
+- `阶段最大连续异常有效小时数`。
 
 判断重点:
 
@@ -415,6 +415,33 @@ python backend/scan_pks_fault_evolution.py \
 峰值是否真实发生在完整运行小时内,是否重复出现,均值是否同步抬升
 ```
 
+### 7.1 振动阶段综合评分
+
+振动阶段使用持续时间、平均振动抬升、阶段最大振动值和峰值稳健偏离进行综合评分,总分 0~100 分:
+
+```text
+持续时间 40 分 + 平均振动 20 分 + 最大振动值 20 分 + 峰值稳健偏离 20 分
+```
+
+评分分级:
+
+- 0~29 分:轻微;
+- 30~59 分:异常;
+- 60~100 分:严重异常。
+
+达到以下任一条件时直接判定为严重异常:峰值达到高高报警阈值、存在高高报警小时,或峰值达到 8 个稳健尺度且阶段至少包含 2 个有效运行小时。
+
+其中:
+
+```text
+近期峰值基线 = 前 336 个有效运行小时的峰值中位数
+稳健尺度 = max(1.4826 × MAD, |近期峰值基线| × 5%, 极小值)
+峰值稳健偏离 = (当前峰值 - 近期峰值基线) / 稳健尺度
+峰值/基线比例 = 当前峰值 / 近期峰值基线
+```
+
+例如峰值稳健偏离 19.43,表示当前峰值高于近期基线 19.43 个稳健尺度;如果峰值/基线比例为 2.41,则表示当前峰值约为基线的 2.41 倍。两者含义不同。
+
 ## 8. 重要限制
 
 1. 算法输出的是异常候选和演化阶段,不是最终故障诊断。

+ 118 - 33
backend/scan_pks_fault_evolution.py

@@ -40,6 +40,7 @@ OIL_DOWNGRADE_HOURS = 2
 OIL_RECOVER_HOURS = 3
 VIBRATION_CONFIRM_HOURS = 3
 VIBRATION_MERGE_GAP_HOURS = 6
+VIBRATION_RESTART_GAP_HOURS = 12
 
 SITE_POINT_COLUMNS = (
     "ItemName, ItemDescription, Units, "
@@ -1194,25 +1195,36 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
         series.sort(key=lambda row: row["_dt"])
         config = configs[batch]
         valid = []
+        last_valid_dt = None
         for row in series:
-            prior = valid[-336:]
-            if len(prior) < MIN_HISTORY_HOURS:
-                valid.append(row)
+            if row["运行覆盖率"] < MIN_HOUR_COVERAGE:
+                continue
+            restart = last_valid_dt is None or row["_dt"] - last_valid_dt > timedelta(hours=VIBRATION_RESTART_GAP_HOURS)
+            if restart:
+                # The first complete running hour after a restart is a
+                # transition hour. Keep older valid hours for the long-term
+                # robust reference, but do not evaluate this transition hour.
+                last_valid_dt = row["_dt"]
                 continue
+            prior = valid[-336:]
             side_signals = []
             for side in ("coupling", "chain"):
                 avg_center, avg_scale = robust_baseline([item[f"{side}_avg"] for item in prior])
                 max_center, max_scale = robust_baseline([item[f"{side}_max"] for item in prior])
                 current_avg = row[f"{side}_avg"]
                 current_max = row[f"{side}_max"]
-                if avg_center is None or max_center is None or current_max is None:
+                if current_max is None:
                     continue
-                avg_z = ((current_avg - avg_center) / avg_scale) if current_avg is not None else 0.0
-                peak_z = (current_max - max_center) / max_scale
+                avg_z = ((current_avg - avg_center) / avg_scale) if avg_center is not None and current_avg is not None else 0.0
+                peak_z = (current_max - max_center) / max_scale if max_center is not None and max_scale else 0.0
                 running = max(row["运行样本数"], 1)
                 high_ratio = (row[f"{side}_high_count"] or 0) / running
                 high_high_ratio = (row[f"{side}_high_high_count"] or 0) / running
                 limits = config[side]
+                local_prior = valid[:6] if len(valid) >= 6 else []
+                local_avg = median_value([item[f"{side}_avg"] for item in local_prior])
+                local_max = median_value([item[f"{side}_max"] for item in local_prior])
+                local_rise = False
                 severe = (
                     high_high_ratio >= 0.01
                     or current_max >= limits["high_high"]
@@ -1223,8 +1235,9 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
                     or current_max >= limits["high"]
                     or peak_z >= 5
                     or avg_z >= 4
+                    or local_rise
                 )
-                mild = peak_z >= 4 or avg_z >= 3
+                mild = peak_z >= 4 or avg_z >= 3 or local_rise
                 if severe or abnormal or mild:
                     side_signals.append({
                         "side": side,
@@ -1237,6 +1250,7 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
                         "value": current_max,
                         "avg": current_avg,
                         "prior_max_center": max_center,
+                        "prior_max_scale": max_scale,
                         "prior_avg_center": avg_center,
                     })
             if side_signals:
@@ -1260,6 +1274,7 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
                     "_reason": ";".join(reasons),
                 })
             valid.append(row)
+            last_valid_dt = row["_dt"]
     return signals
 
 
@@ -1280,11 +1295,39 @@ def merge_vibration_signals(signals: list[dict], configs: dict[int, dict]):
         if current:
             events.append(build_vibration_event(current, configs[batch]))
 
+    for index, event in enumerate(events, 1):
+        event["周期编号"] = f'{event["机组"]}-V{index:02d}'
+
     filtered = []
     for event in events:
-        if event["严重信号小时数"] > 0 or event["触发小时数"] >= 2:
+        # A single post-start relative-rise signal is not sufficient evidence;
+        # absolute severe signals remain eligible on their own.
+        if event["_严重信号小时数"] > 0 or event["_触发小时数"] >= 2:
             filtered.append(event)
-    return sorted(filtered, key=lambda row: (row["批次"], row["开始时间"]))
+    return sorted(filtered, key=lambda row: (row["批次"], row["开始小时"]))
+
+
+def vibration_score(duration_hours: int, valid_hours: int, avg_change: float, max_value: float, peak_z: float, high_hours: int, high_high_hours: int, config: dict, side: str):
+    duration_score = min(40.0, max(0.0, (duration_hours - 1) / 12 * 40))
+    avg_score = min(20.0, max(0.0, (avg_change - 0.10) / 0.40 * 20)) if avg_change >= 0.10 else 0.0
+    high = config[side]["high"]
+    high_high = config[side]["high_high"]
+    if max_value < high:
+        max_score = min(10.0, max(0.0, (max_value / max(high, 1e-6) - 0.8) / 0.2 * 10))
+    elif max_value < high_high:
+        max_score = 10.0 + (max_value - high) / max(high_high - high, 1e-6) * 10.0
+    else:
+        max_score = 20.0
+    peak_score = min(20.0, max(0.0, (peak_z - 4.0) / 8.0 * 20.0)) if peak_z >= 4.0 else 0.0
+    total = duration_score + avg_score + max_score + peak_score
+    hard_severe = max_value >= high_high or high_high_hours > 0 or (peak_z >= 8.0 and valid_hours >= 2)
+    if hard_severe or total >= 60.0:
+        level = "严重异常"
+    elif total >= 30.0:
+        level = "异常"
+    else:
+        level = "轻微"
+    return total, level, duration_score, avg_score, max_score, peak_score
 
 
 def build_vibration_event(signals: list[dict], config: dict):
@@ -1332,29 +1375,69 @@ def build_vibration_event(signals: list[dict], config: dict):
         shape = "混合型"
 
     peak_signal = max(signals, key=lambda signal: signal["_strongest"]["peak_z"])
+    first_signal = signals[0]
+    last_signal = signals[-1]
+    first_value = first_signal["_strongest"]["value"]
+    last_value = last_signal["_strongest"]["value"]
+    baseline = strongest["prior_max_center"]
+    scale = max(strongest.get("prior_max_scale", 0.0), 0.0)
+    normal_band = f"{baseline - 2 * scale:.3f}~{baseline + 2 * scale:.3f} mm/s" if baseline is not None else ""
+    consecutive = 0
+    longest_consecutive = 0
+    for signal in signals:
+        if signal["_level"] >= 2:
+            consecutive += 1
+            longest_consecutive = max(longest_consecutive, consecutive)
+        else:
+            consecutive = 0
+    score, level, duration_score, avg_score, max_score, peak_score = vibration_score(
+        duration_hours, len(signals), avg_change, max_value, max_peak_z,
+        high_hours, high_high_hours, config, strongest["side"]
+    )
+    confirmed = "是" if level != "轻微" else "否"
+    if shape == "持续抬升型":
+        basis = f"{main_side}振动均值持续抬升,阶段内{longest_consecutive or len(signals)}个有效运行小时触发"
+    elif shape == "间歇冲击型":
+        basis = f"{main_side}重复出现振动冲击,阶段内{len(signals)}个有效运行小时触发"
+    else:
+        basis = f"{main_side}振动峰值或均值偏离近期基线,阶段内{len(signals)}个有效运行小时触发"
+    if high_high_hours:
+        basis += f";高高报警小时{high_high_hours}个"
+    elif high_hours:
+        basis += f";高报警小时{high_hours}个"
+    basis += f";最大振动值{max_value:.3f}mm/s,峰值高于近期基线{max_peak_z:.2f}个稳健尺度(峰值/基线{peak_ratio:.2f}倍);综合评分{score:.1f}分"
     return {
         "机组": config["unit"],
         "批次": config["batch"],
-        "开始时间": start.strftime("%Y-%m-%d %H:%M:%S"),
-        "结束时间": end.strftime("%Y-%m-%d %H:%M:%S"),
-        "峰值时间": peak_signal["_dt"].strftime("%Y-%m-%d %H:%M:%S"),
-        "振动等级": level,
-        "变化形态": shape,
-        "主要振动侧": main_side,
-        "持续小时数": duration_hours,
-        "持续运行日数": running_days,
-        "触发小时数": len(signals),
-        "高报警小时数": high_hours,
-        "高高报警小时数": high_high_hours,
-        "严重信号小时数": severe_hours,
-        "最大振动值": max_value,
-        "联轴器端高报警阈值": config["coupling"]["high"],
-        "联轴器端高高报警阈值": config["coupling"]["high_high"],
-        "链轮端高报警阈值": config["chain"]["high"],
-        "链轮端高高报警阈值": config["chain"]["high_high"],
-        "最大峰值相对近期基线偏离": max_peak_z,
-        "振动均值相对近期基线变化": avg_change,
-        "触发原因": ";".join(signal["_reason"] for signal in signals),
+        "周期编号": "",
+        "振动阶段": level,
+        "开始小时": start.strftime("%Y-%m-%d %H:%M:%S"),
+        "结束小时": end.strftime("%Y-%m-%d %H:%M:%S"),
+        "持续自然小时数": duration_hours,
+        "有效运行小时数": len(signals),
+        "近期基线振动": baseline,
+        "正常波动带": normal_band,
+        "阶段开始小时振动": first_value,
+        "阶段结束小时振动": last_value,
+        "阶段最大振动值": max_value,
+        "阶段开始相对近期基线变化": (first_value / baseline - 1) if baseline else None,
+        "阶段结束相对近期基线变化": (last_value / baseline - 1) if baseline else None,
+        "阶段最大连续异常有效小时数": longest_consecutive,
+        "阶段综合评分": score,
+        "持续时间得分": duration_score,
+        "平均振动得分": avg_score,
+        "最大振动值得分": max_score,
+        "峰值偏离得分": peak_score,
+        "最强异常侧": main_side,
+        "最强侧近期峰值基线": baseline,
+        "最强侧近期稳健尺度": scale,
+        "阶段最大峰值/基线比例": peak_ratio,
+        "阶段高报警小时数": high_hours,
+        "阶段高高报警小时数": high_high_hours,
+        "_严重信号小时数": severe_hours,
+        "_触发小时数": len(signals),
+        "是否形成确认等级": confirmed,
+        "阶段触发依据": basis,
     }
 
 
@@ -1501,10 +1584,12 @@ def main():
     write_csv(out / "润滑油压力演化周期.csv", clean_rows(oil_cycles), cycle_fields, {field: field for field in cycle_fields})
 
     vibration_fields = [
-        "机组", "批次", "开始时间", "结束时间", "峰值时间", "振动等级", "变化形态", "主要振动侧",
-        "持续小时数", "持续运行日数", "触发小时数", "高报警小时数", "高高报警小时数", "严重信号小时数",
-        "最大振动值", "联轴器端高报警阈值", "联轴器端高高报警阈值", "链轮端高报警阈值",
-        "链轮端高高报警阈值", "最大峰值相对近期基线偏离", "振动均值相对近期基线变化", "触发原因",
+        "机组", "批次", "周期编号", "振动阶段", "开始小时", "结束小时", "持续自然小时数", "有效运行小时数",
+        "近期基线振动", "正常波动带", "阶段开始小时振动", "阶段结束小时振动", "阶段最大振动值",
+        "阶段开始相对近期基线变化", "阶段结束相对近期基线变化", "阶段最大连续异常有效小时数",
+        "阶段高报警小时数", "阶段高高报警小时数", "是否形成确认等级", "阶段触发依据",
+        "阶段综合评分", "持续时间得分", "平均振动得分", "最大振动值得分", "峰值偏离得分",
+        "最强异常侧", "最强侧近期峰值基线", "最强侧近期稳健尺度", "阶段最大峰值/基线比例",
     ]
     write_csv(out / "振动异常段.csv", clean_rows(vibration_events), vibration_fields, {field: field for field in vibration_fields})
 

Різницю між файлами не показано, бо вона завелика
+ 26 - 27
cache/pks_scanner/evolution_pct_confirmed/振动异常段.csv