pitch.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """
  2. Module 3: 变桨系统异常检测
  3. 算法: LocalOutlierFactor (LOF)
  4. - 基于局部密度,适合检测多桨叶不一致的局部异常
  5. - novelty=True 支持 fit/predict 分离(训练集拟合,推理时预测新数据)
  6. - n_neighbors 自适应:min(20, len(train) // 50),避免小样本退化
  7. 子检测器:
  8. A. PitchRegulationDetector - 桨距角调节异常(设定值 vs 实际值,3个桨叶)
  9. B. PitchCoordDetector - 变桨-转速-功率协调异常
  10. C. MinPitchDetector - 最小桨距角异常(保留 IsolationForest,分布异常更合适)
  11. """
  12. import pandas as pd
  13. import numpy as np
  14. from sklearn.neighbors import LocalOutlierFactor
  15. from sklearn.ensemble import IsolationForest
  16. from sklearn.preprocessing import StandardScaler
  17. import joblib
  18. from pathlib import Path
  19. from config import (
  20. COL_PITCH_SET_1, COL_PITCH_SET_2, COL_PITCH_SET_3,
  21. COL_PITCH_ACT_1, COL_PITCH_ACT_2, COL_PITCH_ACT_3,
  22. COL_PITCH_SPD_1, COL_PITCH_SPD_2, COL_PITCH_SPD_3,
  23. COL_ROTOR_SPD, COL_P_ACTIVE,
  24. ISO_CONTAMINATION, ISO_RANDOM_STATE, ISO_N_ESTIMATORS,
  25. )
  26. PITCH_SET_COLS = [COL_PITCH_SET_1, COL_PITCH_SET_2, COL_PITCH_SET_3]
  27. PITCH_ACT_COLS = [COL_PITCH_ACT_1, COL_PITCH_ACT_2, COL_PITCH_ACT_3]
  28. PITCH_SPD_COLS = [COL_PITCH_SPD_1, COL_PITCH_SPD_2, COL_PITCH_SPD_3]
  29. # ── A. 桨距角调节检测器 (LOF) ──────────────────────────────────────────────────
  30. class PitchRegulationDetector:
  31. """
  32. 特征:
  33. - 每个桨叶的 (设定值-实际值) 偏差
  34. - 三桨叶实际值不一致度(std)
  35. - 三桨叶变桨速度均值、不一致度(若 pitch_spd_1/2/3 存在)
  36. LOF 检测局部密度异常,适合多桨叶不同步场景。
  37. """
  38. def __init__(self, n_neighbors: int = 20, contamination: float = ISO_CONTAMINATION):
  39. self.n_neighbors = n_neighbors
  40. self.contamination = contamination
  41. self.scaler = StandardScaler()
  42. self.model = LocalOutlierFactor(
  43. n_neighbors=n_neighbors,
  44. contamination=contamination,
  45. novelty=True,
  46. )
  47. def _features(self, df: pd.DataFrame) -> pd.DataFrame:
  48. feat = {}
  49. for i, (s, a) in enumerate(zip(PITCH_SET_COLS, PITCH_ACT_COLS), 1):
  50. if s in df.columns and a in df.columns:
  51. feat[f"err_{i}"] = df[s] - df[a]
  52. elif a in df.columns:
  53. feat[f"err_{i}"] = pd.Series(np.nan, index=df.index)
  54. act_cols = [c for c in PITCH_ACT_COLS if c in df.columns]
  55. if len(act_cols) >= 2:
  56. feat["act_std"] = df[act_cols].std(axis=1)
  57. # 变桨速度特征(可选)
  58. spd_cols = [c for c in PITCH_SPD_COLS if c in df.columns]
  59. if len(spd_cols) >= 2:
  60. spd_df = df[spd_cols]
  61. feat["spd_mean"] = spd_df.mean(axis=1)
  62. feat["spd_std"] = spd_df.std(axis=1)
  63. return pd.DataFrame(feat, index=df.index).dropna()
  64. def fit(self, df: pd.DataFrame) -> "PitchRegulationDetector":
  65. feat = self._features(df)
  66. if feat.empty:
  67. raise ValueError("变桨调节特征为空,检查测点是否存在")
  68. # 自适应 n_neighbors:避免小样本时 n_neighbors 过大导致 LOF 退化
  69. adaptive_k = max(5, min(self.n_neighbors, len(feat) // 50))
  70. if adaptive_k != self.n_neighbors:
  71. self.model = LocalOutlierFactor(
  72. n_neighbors=adaptive_k,
  73. contamination=self.contamination,
  74. novelty=True,
  75. )
  76. X = self.scaler.fit_transform(feat)
  77. self.model.fit(X)
  78. return self
  79. def predict(self, df: pd.DataFrame) -> pd.DataFrame:
  80. out = pd.DataFrame({"anomaly": False, "score": np.nan}, index=df.index)
  81. feat = self._features(df)
  82. if feat.empty:
  83. return out
  84. X = self.scaler.transform(feat)
  85. out.loc[feat.index, "anomaly"] = self.model.predict(X) == -1
  86. out.loc[feat.index, "score"] = self.model.score_samples(X)
  87. return out
  88. def save(self, path: Path):
  89. joblib.dump(self, path)
  90. @classmethod
  91. def load(cls, path: Path) -> "PitchRegulationDetector":
  92. return joblib.load(path)
  93. # ── B. 变桨-转速-功率协调检测器 (LOF) ─────────────────────────────────────────
  94. class PitchCoordDetector:
  95. """
  96. 特征: pitch_ang_act_1, rotor_spd, p_active 及衍生比值。
  97. 优化: 若三桨叶均存在,加入三桨叶均值、不一致度(std)特征,
  98. 替代单桨叶 pitch_ang_act_1,捕捉三桨叶整体协调异常。
  99. LOF 检测三者协调关系的局部偏离。
  100. """
  101. REQUIRED = [COL_PITCH_ACT_1, COL_ROTOR_SPD, COL_P_ACTIVE]
  102. def __init__(self, n_neighbors: int = 20, contamination: float = ISO_CONTAMINATION):
  103. self.n_neighbors = n_neighbors
  104. self.contamination = contamination
  105. self.scaler = StandardScaler()
  106. self.model = LocalOutlierFactor(
  107. n_neighbors=n_neighbors,
  108. contamination=contamination,
  109. novelty=True,
  110. )
  111. def _features(self, df: pd.DataFrame) -> pd.DataFrame:
  112. if COL_ROTOR_SPD not in df.columns or COL_P_ACTIVE not in df.columns:
  113. return pd.DataFrame()
  114. d = df[[COL_ROTOR_SPD, COL_P_ACTIVE]].copy()
  115. # 低转速时 p_per_spd 极度放大噪声,过滤掉
  116. d = d[d[COL_ROTOR_SPD] > 5.0]
  117. # 三桨叶一致性特征(优先使用全部三桨叶)
  118. act_cols = [c for c in PITCH_ACT_COLS if c in df.columns]
  119. if len(act_cols) >= 2:
  120. pitch_df = df[act_cols].loc[d.index]
  121. d["pitch_mean"] = pitch_df.mean(axis=1)
  122. d["pitch_std"] = pitch_df.std(axis=1)
  123. elif COL_PITCH_ACT_1 in df.columns:
  124. d["pitch_mean"] = df[COL_PITCH_ACT_1].loc[d.index]
  125. else:
  126. return pd.DataFrame()
  127. d = d.dropna()
  128. d["p_per_spd"] = d[COL_P_ACTIVE] / d[COL_ROTOR_SPD]
  129. d["pitch_x_spd"] = d["pitch_mean"] * d[COL_ROTOR_SPD]
  130. return d.dropna()
  131. def fit(self, df: pd.DataFrame) -> "PitchCoordDetector":
  132. feat = self._features(df)
  133. if feat.empty:
  134. raise ValueError("变桨协调特征为空,检查测点是否存在")
  135. # 自适应 n_neighbors
  136. adaptive_k = max(5, min(self.n_neighbors, len(feat) // 50))
  137. if adaptive_k != self.n_neighbors:
  138. self.model = LocalOutlierFactor(
  139. n_neighbors=adaptive_k,
  140. contamination=self.contamination,
  141. novelty=True,
  142. )
  143. X = self.scaler.fit_transform(feat)
  144. self.model.fit(X)
  145. return self
  146. def predict(self, df: pd.DataFrame) -> pd.DataFrame:
  147. out = pd.DataFrame({"anomaly": False, "score": np.nan}, index=df.index)
  148. feat = self._features(df)
  149. if feat.empty:
  150. return out
  151. X = self.scaler.transform(feat)
  152. out.loc[feat.index, "anomaly"] = self.model.predict(X) == -1
  153. out.loc[feat.index, "score"] = self.model.score_samples(X)
  154. return out
  155. def save(self, path: Path):
  156. joblib.dump(self, path)
  157. @classmethod
  158. def load(cls, path: Path) -> "PitchCoordDetector":
  159. return joblib.load(path)
  160. # ── C. 最小桨距角检测器 (IsolationForest) ─────────────────────────────────────
  161. class MinPitchDetector:
  162. """
  163. 特征: 三桨叶实际值的最小值、均值、极差。
  164. 保留 IsolationForest:最小桨距角是全局分布异常,IF 更合适。
  165. """
  166. def __init__(self, contamination: float = ISO_CONTAMINATION):
  167. self.scaler = StandardScaler()
  168. self.model = IsolationForest(
  169. n_estimators=ISO_N_ESTIMATORS,
  170. contamination=contamination,
  171. random_state=ISO_RANDOM_STATE,
  172. )
  173. def _features(self, df: pd.DataFrame) -> pd.DataFrame:
  174. act_cols = [c for c in PITCH_ACT_COLS if c in df.columns]
  175. if not act_cols:
  176. return pd.DataFrame()
  177. d = df[act_cols].dropna()
  178. return pd.DataFrame({
  179. "min_pitch": d.min(axis=1),
  180. "mean_pitch": d.mean(axis=1),
  181. "range_pitch": d.max(axis=1) - d.min(axis=1),
  182. }, index=d.index)
  183. def fit(self, df: pd.DataFrame) -> "MinPitchDetector":
  184. feat = self._features(df)
  185. if feat.empty:
  186. raise ValueError("最小桨距角特征为空,检查测点是否存在")
  187. X = self.scaler.fit_transform(feat)
  188. self.model.fit(X)
  189. return self
  190. def predict(self, df: pd.DataFrame) -> pd.DataFrame:
  191. out = pd.DataFrame({"anomaly": False, "score": np.nan}, index=df.index)
  192. feat = self._features(df)
  193. if feat.empty:
  194. return out
  195. X = self.scaler.transform(feat)
  196. out.loc[feat.index, "anomaly"] = self.model.predict(X) == -1
  197. out.loc[feat.index, "score"] = self.model.score_samples(X)
  198. return out
  199. def save(self, path: Path):
  200. joblib.dump(self, path)
  201. @classmethod
  202. def load(cls, path: Path) -> "MinPitchDetector":
  203. return joblib.load(path)