Procházet zdrojové kódy

功图:全事件修前/修后同级对照脚本;四缸轴侧判定为配置变化

18922397810 před 3 týdny
rodič
revize
880da4ffbb

+ 125 - 0
backend/indicator_features/repair_compare.py

@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+"""Compare indicator features between a pre-repair and a post-repair segment.
+
+For each cylinder/side it reports the segment medians and, importantly, the
+*peer-relative* value (deviation from the same-stage/side median), so a repair
+that only changes one cylinder can be separated from a process-wide shift.
+Read-only.
+"""
+from __future__ import annotations
+
+import argparse
+import sys
+from collections import defaultdict
+from pathlib import Path
+
+import numpy as np
+
+BACKEND = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(BACKEND))
+
+from indicator_features.extract_from_db import (  # noqa: E402
+    CYLINDERS,
+    SIDES,
+    connect,
+    fetch_cycles,
+    fetch_file_rows,
+    point_name,
+    sample_per_day,
+)
+from indicator_features.indicator_features import CYLINDER_STAGE, cycle_features  # noqa: E402
+
+FEATURES = ("p_discharge", "pressure_ratio", "discharge_frac", "indicated_work", "efficiency", "n_comp")
+
+
+def extract(connection, unit, cylinders, sides, start, end, per_day):
+    rows = []
+    for cylinder in cylinders:
+        for side in sides:
+            point = point_name(unit, cylinder, side)
+            files = fetch_file_rows(connection, point, start, end)
+            picked = sample_per_day(files, per_day)
+            if not picked:
+                continue
+            id_to_time = {int(r["id"]): r for r in picked}
+            for file_id, signal in fetch_cycles(connection, list(id_to_time), 200).items():
+                feats = cycle_features(signal, cylinder)
+                if feats:
+                    rows.append({"cylinder": cylinder, "side": side, "stage": CYLINDER_STAGE[cylinder], **feats})
+    return rows
+
+
+def summary(rows):
+    table = {}
+    groups = defaultdict(list)
+    for row in rows:
+        groups[(row["cylinder"], row["side"])].append(row)
+    for key, items in groups.items():
+        table[key] = {feature: float(np.median([r[feature] for r in items if r[feature] is not None]))
+                      for feature in FEATURES}
+    return table
+
+
+def peer_relative(table, feature):
+    """Deviation from the same-stage/side median, per cylinder."""
+    by_stage = defaultdict(dict)
+    for (cylinder, side), values in table.items():
+        stage = CYLINDER_STAGE[cylinder]
+        by_stage[(stage, side)][cylinder] = values[feature]
+    result = {}
+    for (stage, side), members in by_stage.items():
+        clean = {c: v for c, v in members.items() if v is not None}
+        if len(clean) < 2:
+            continue
+        med = float(np.median(list(clean.values())))
+        if abs(med) < 1e-9:
+            continue
+        for cylinder, value in clean.items():
+            result[(cylinder, side)] = (value - med) / med
+    return result
+
+
+def main():
+    parser = argparse.ArgumentParser(description="修前/修后功图特征同级对照")
+    parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
+    parser.add_argument("--cylinders", nargs="+", default=list(CYLINDERS), choices=CYLINDERS)
+    parser.add_argument("--sides", nargs="+", default=list(SIDES), choices=SIDES)
+    parser.add_argument("--pre", nargs=2, required=True, metavar=("START", "END"))
+    parser.add_argument("--post", nargs=2, required=True, metavar=("START", "END"))
+    parser.add_argument("--per-day", type=int, default=5)
+    args = parser.parse_args()
+
+    connection = connect()
+    try:
+        pre_rows = extract(connection, args.unit, args.cylinders, args.sides, args.pre[0], args.pre[1], args.per_day)
+        post_rows = extract(connection, args.unit, args.cylinders, args.sides, args.post[0], args.post[1], args.per_day)
+    finally:
+        connection.close()
+
+    pre, post = summary(pre_rows), summary(post_rows)
+    print(f"{args.unit}号机  修前 {args.pre[0]}~{args.pre[1]} ({len(pre_rows)}行)  修后 {args.post[0]}~{args.post[1]} ({len(post_rows)}行)")
+    if not pre_rows or not post_rows:
+        print("某一段无数据"); return
+
+    print(f"\n{'缸侧':>6} " + ' '.join(f"{f[:11]:>18}" for f in FEATURES))
+    for key in sorted(set(pre) & set(post)):
+        cells = []
+        for feature in FEATURES:
+            a, b = pre[key].get(feature), post[key].get(feature)
+            if a and b and abs(a) > 1e-9:
+                cells.append(f"{a:7.3f}->{b:6.3f}({(b-a)/a*100:+5.1f}%)")
+            else:
+                cells.append(f"{'':>18}")
+        print(f"{key[0]+key[1]:>6} " + ' '.join(f"{c:>18}" for c in cells))
+
+    print("\n同级 peer 相对(修前 -> 修后):")
+    for feature in ("pressure_ratio", "p_discharge", "efficiency"):
+        rel_pre = peer_relative(pre, feature)
+        rel_post = peer_relative(post, feature)
+        print(f"  {feature}:")
+        for key in sorted(set(rel_pre) & set(rel_post)):
+            print(f"     {key[0]}缸{key[1]}: {rel_pre[key]*100:+.1f}% -> {rel_post[key]*100:+.1f}%")
+
+
+if __name__ == "__main__":
+    main()

+ 13 - 1
backend/indicator_features/观察记录.md

@@ -57,7 +57,19 @@
 
 - **四缸轴侧压比(相对同级)在维修前后由 +34% 降到 +13%**,相对稳定缸(二缸)降约 16%;`efficiency` 同期 0.558→0.626(+12%)。
 - 其余一级缸变化仅 ±2~10%。这是目前**唯一**在维修前后明显变化的缸级特征。
-- 注意:中间有 03-20~04-18 数据缺口,且同期工况(压比中位 3.42→3.10)也变了,所以尚不能完全归因于维修;需更多事件确认。
+- **对照分析(任务 2)——结论:大概率是配置变化,不是纯故障**:
+
+  | 时期 | 四缸/二缸 压比 | 同级排序 |
+  |---|---:|---|
+  | 2025-03~05(无维修) | 0.96~0.98 | 二 > 四 > 六 |
+  | 2026-03-15~20(修前) | 1.27~1.34 | 四 > 二 > 六 |
+  | 2026-04~06(修后) | 1.12~1.17 | 四 > 二 > 六 |
+
+  四缸轴侧相对二缸在 2025→2026 之间发生**持久排序反转**(2026 全年仍偏高,维修后也回不到 2025 水平)→ 更像**年度拆装/配置变化**;维修只解释 1.34→1.12 这一部分。
+- **全事件修前/修后对照**(`repair_compare.py`):
+  - 8 号机 2026.3.29/4.1:仅四缸轴侧显著(见上);
+  - 7 号机 2025.8.8、2026.5.21 气阀:各缸 peer 相对仅 ±1~2%,**无恢复特征**;
+  - 其余活塞事件无可用的故障前段。
 
 ### 4.1 8 号机 2025 春季(活塞事件前最后运行段)
 - 全通道正常;**二缸盖侧** `p_discharge` 比同级低约 8%、指示功低约 9%,但**二缸轴侧正常** → 倾向单侧偏置,不是活塞双腔特征。