|
@@ -40,6 +40,7 @@ OIL_DOWNGRADE_HOURS = 2
|
|
|
OIL_RECOVER_HOURS = 3
|
|
OIL_RECOVER_HOURS = 3
|
|
|
VIBRATION_CONFIRM_HOURS = 3
|
|
VIBRATION_CONFIRM_HOURS = 3
|
|
|
VIBRATION_MERGE_GAP_HOURS = 6
|
|
VIBRATION_MERGE_GAP_HOURS = 6
|
|
|
|
|
+VIBRATION_RESTART_GAP_HOURS = 12
|
|
|
|
|
|
|
|
SITE_POINT_COLUMNS = (
|
|
SITE_POINT_COLUMNS = (
|
|
|
"ItemName, ItemDescription, Units, "
|
|
"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"])
|
|
series.sort(key=lambda row: row["_dt"])
|
|
|
config = configs[batch]
|
|
config = configs[batch]
|
|
|
valid = []
|
|
valid = []
|
|
|
|
|
+ last_valid_dt = None
|
|
|
for row in series:
|
|
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
|
|
continue
|
|
|
|
|
+ prior = valid[-336:]
|
|
|
side_signals = []
|
|
side_signals = []
|
|
|
for side in ("coupling", "chain"):
|
|
for side in ("coupling", "chain"):
|
|
|
avg_center, avg_scale = robust_baseline([item[f"{side}_avg"] for item in prior])
|
|
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])
|
|
max_center, max_scale = robust_baseline([item[f"{side}_max"] for item in prior])
|
|
|
current_avg = row[f"{side}_avg"]
|
|
current_avg = row[f"{side}_avg"]
|
|
|
current_max = row[f"{side}_max"]
|
|
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
|
|
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)
|
|
running = max(row["运行样本数"], 1)
|
|
|
high_ratio = (row[f"{side}_high_count"] or 0) / running
|
|
high_ratio = (row[f"{side}_high_count"] or 0) / running
|
|
|
high_high_ratio = (row[f"{side}_high_high_count"] or 0) / running
|
|
high_high_ratio = (row[f"{side}_high_high_count"] or 0) / running
|
|
|
limits = config[side]
|
|
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 = (
|
|
severe = (
|
|
|
high_high_ratio >= 0.01
|
|
high_high_ratio >= 0.01
|
|
|
or current_max >= limits["high_high"]
|
|
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 current_max >= limits["high"]
|
|
|
or peak_z >= 5
|
|
or peak_z >= 5
|
|
|
or avg_z >= 4
|
|
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:
|
|
if severe or abnormal or mild:
|
|
|
side_signals.append({
|
|
side_signals.append({
|
|
|
"side": side,
|
|
"side": side,
|
|
@@ -1237,6 +1250,7 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
|
|
|
"value": current_max,
|
|
"value": current_max,
|
|
|
"avg": current_avg,
|
|
"avg": current_avg,
|
|
|
"prior_max_center": max_center,
|
|
"prior_max_center": max_center,
|
|
|
|
|
+ "prior_max_scale": max_scale,
|
|
|
"prior_avg_center": avg_center,
|
|
"prior_avg_center": avg_center,
|
|
|
})
|
|
})
|
|
|
if side_signals:
|
|
if side_signals:
|
|
@@ -1260,6 +1274,7 @@ def vibration_hour_signals(rows: list[dict], configs: dict[int, dict]):
|
|
|
"_reason": ";".join(reasons),
|
|
"_reason": ";".join(reasons),
|
|
|
})
|
|
})
|
|
|
valid.append(row)
|
|
valid.append(row)
|
|
|
|
|
+ last_valid_dt = row["_dt"]
|
|
|
return signals
|
|
return signals
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1280,11 +1295,39 @@ def merge_vibration_signals(signals: list[dict], configs: dict[int, dict]):
|
|
|
if current:
|
|
if current:
|
|
|
events.append(build_vibration_event(current, configs[batch]))
|
|
events.append(build_vibration_event(current, configs[batch]))
|
|
|
|
|
|
|
|
|
|
+ for index, event in enumerate(events, 1):
|
|
|
|
|
+ event["周期编号"] = f'{event["机组"]}-V{index:02d}'
|
|
|
|
|
+
|
|
|
filtered = []
|
|
filtered = []
|
|
|
for event in events:
|
|
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)
|
|
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):
|
|
def build_vibration_event(signals: list[dict], config: dict):
|
|
@@ -1332,29 +1375,69 @@ def build_vibration_event(signals: list[dict], config: dict):
|
|
|
shape = "混合型"
|
|
shape = "混合型"
|
|
|
|
|
|
|
|
peak_signal = max(signals, key=lambda signal: signal["_strongest"]["peak_z"])
|
|
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 {
|
|
return {
|
|
|
"机组": config["unit"],
|
|
"机组": config["unit"],
|
|
|
"批次": config["batch"],
|
|
"批次": 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})
|
|
write_csv(out / "润滑油压力演化周期.csv", clean_rows(oil_cycles), cycle_fields, {field: field for field in cycle_fields})
|
|
|
|
|
|
|
|
vibration_fields = [
|
|
vibration_fields = [
|
|
|
- "机组", "批次", "开始时间", "结束时间", "峰值时间", "振动等级", "变化形态", "主要振动侧",
|
|
|
|
|
- "持续小时数", "持续运行日数", "触发小时数", "高报警小时数", "高高报警小时数", "严重信号小时数",
|
|
|
|
|
- "最大振动值", "联轴器端高报警阈值", "联轴器端高高报警阈值", "链轮端高报警阈值",
|
|
|
|
|
- "链轮端高高报警阈值", "最大峰值相对近期基线偏离", "振动均值相对近期基线变化", "触发原因",
|
|
|
|
|
|
|
+ "机组", "批次", "周期编号", "振动阶段", "开始小时", "结束小时", "持续自然小时数", "有效运行小时数",
|
|
|
|
|
+ "近期基线振动", "正常波动带", "阶段开始小时振动", "阶段结束小时振动", "阶段最大振动值",
|
|
|
|
|
+ "阶段开始相对近期基线变化", "阶段结束相对近期基线变化", "阶段最大连续异常有效小时数",
|
|
|
|
|
+ "阶段高报警小时数", "阶段高高报警小时数", "是否形成确认等级", "阶段触发依据",
|
|
|
|
|
+ "阶段综合评分", "持续时间得分", "平均振动得分", "最大振动值得分", "峰值偏离得分",
|
|
|
|
|
+ "最强异常侧", "最强侧近期峰值基线", "最强侧近期稳健尺度", "阶段最大峰值/基线比例",
|
|
|
]
|
|
]
|
|
|
write_csv(out / "振动异常段.csv", clean_rows(vibration_events), vibration_fields, {field: field for field in vibration_fields})
|
|
write_csv(out / "振动异常段.csv", clean_rows(vibration_events), vibration_fields, {field: field for field in vibration_fields})
|
|
|
|
|
|