ClassIdentifier.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import datetime
  2. import traceback
  3. import numpy as np
  4. from pandas import DataFrame
  5. from utils.log.trans_log import logger
  6. class ClassIdentifier(object):
  7. """
  8. 分类标识 -1:停机 0:好点 1:欠发功率点;2:超发功率点;3:额定风速以上的超发功率点 4: 限电
  9. """
  10. def __init__(self, wind_turbine_number="", origin_df: DataFrame = None,
  11. wind_velocity='wind_velocity',
  12. active_power='active_power',
  13. pitch_angle_blade='pitch_angle_blade_1',
  14. rated_power=1500, cut_out_speed=20):
  15. """
  16. :param origin_df: The pandas DataFrame containing the input data.
  17. :param wind_velocity: 风速字段
  18. :param active_power: 有功功率字段
  19. :param pitch_angle_blade: 桨距角
  20. :param rated_power: 额定功率
  21. :param cut_out_speed: 切出风速
  22. """
  23. self.wind_turbine_number = wind_turbine_number
  24. self.wind_velocity = wind_velocity
  25. self.active_power = active_power
  26. self.pitch_angle_blade = pitch_angle_blade
  27. self.rated_power = rated_power # 额定功率1500kw,可改为2000kw
  28. self.cut_out_speed = cut_out_speed
  29. if self.rated_power is None:
  30. logger.info(f"{wind_turbine_number} WARNING:rated_power配置为空的")
  31. self.rated_power = 1500
  32. if self.cut_out_speed is None:
  33. logger.info(f"{cut_out_speed} WARNING:cut_out_speed配置为空的")
  34. self.cut_out_speed = 20
  35. self.df = origin_df
  36. def identifier(self):
  37. # 风速 和 有功功率 df
  38. # self.df = self.df[[self.wind_velocity, self.active_power, "pitch_angle_blade_1"]]
  39. self.df.reset_index(inplace=True)
  40. if len(self.df[self.active_power].unique()) <= 10:
  41. self.df['lab'] = -1
  42. return self.df
  43. wind_and_power_df_count = self.df.shape[0]
  44. power_max = self.df[self.active_power].max()
  45. power_rated = np.ceil(power_max / 100) * 100
  46. v_cut_out = self.cut_out_speed
  47. # 网格法确定风速风向分区数量,功率方向分区数量,
  48. power_bin_count = int(np.ceil(power_rated / 25)) # 功率分区间隔25kW
  49. velocity_bin_count = int(np.ceil(v_cut_out / 0.25)) # 风速分区间隔0.25m/s
  50. # 存储功率大于零的运行数据
  51. power_gt_zero_array = np.zeros([wind_and_power_df_count, 2], dtype=float)
  52. power_gt_zero_array_count = 0
  53. for i in range(wind_and_power_df_count):
  54. if self.df.loc[i, self.active_power] > 0:
  55. power_gt_zero_array[power_gt_zero_array_count, 0] = self.df.loc[i, self.wind_velocity]
  56. power_gt_zero_array[power_gt_zero_array_count, 1] = self.df.loc[i, self.active_power]
  57. power_gt_zero_array_count = power_gt_zero_array_count + 1
  58. # 统计各网格落入的散点个数
  59. x_box_number = np.zeros([power_bin_count, velocity_bin_count], dtype=int)
  60. n_which_p = -1
  61. n_which_v = -1
  62. for i in range(power_gt_zero_array_count):
  63. for m in range(power_bin_count):
  64. if m * 25 < power_gt_zero_array[i, 1] <= (m + 1) * 25:
  65. n_which_p = m
  66. break
  67. for n in range(velocity_bin_count):
  68. if (n * 0.25 + 0.125) < power_gt_zero_array[i, 0] <= ((n + 1) * 0.25 + 0.125):
  69. n_which_v = n
  70. break
  71. if n_which_p > -1 and n_which_v > -1:
  72. x_box_number[n_which_p, n_which_v] = x_box_number[n_which_p, n_which_v] + 1
  73. # 在功率方向将网格内散点绝对个数转换为相对百分比,备用
  74. power_box_percent = np.zeros([power_bin_count, velocity_bin_count], dtype=float)
  75. # 功率方向统计
  76. power_bin_sum = np.zeros(power_bin_count, dtype=int)
  77. for i in range(power_bin_count):
  78. power_bin_sum[i] = sum(x_box_number[i, :])
  79. # for m in range(velocity_bin_count):
  80. # power_bin_sum[i] = power_bin_sum[i] + x_box_number[i, m]
  81. for m in range(velocity_bin_count):
  82. if power_bin_sum[i] > 0:
  83. power_box_percent[i, m] = x_box_number[i, m] / power_bin_sum[i] * 100
  84. # 在风速方向将网格内散点绝对个数转换为相对百分比,备用
  85. v_box_percent = np.zeros([power_bin_count, velocity_bin_count], dtype=float)
  86. v_bin_sum = np.zeros(velocity_bin_count, dtype=int)
  87. for i in range(velocity_bin_count):
  88. v_bin_sum[i] = sum(x_box_number[:, i])
  89. # for m in range(power_bin_count):
  90. # v_bin_sum[i] = v_bin_sum[i] + x_box_number[m, i]
  91. for m in range(power_bin_count):
  92. if v_bin_sum[i] > 0:
  93. v_box_percent[m, i] = x_box_number[m, i] / v_bin_sum[i] * 100
  94. # 以水平功率带方向为准,分析每个水平功率带中,功率主带中心,即找百分比最大的网格位置。
  95. p_box_max_index = np.zeros(power_bin_count, dtype=int) # 水平功率带最大网格位置索引
  96. p_box_max_p = np.zeros(power_bin_count, dtype=int) # 水平功率带最大网格百分比
  97. for m in range(power_bin_count):
  98. # 确定每一水平功率带的最大网格位置索引即百分比值
  99. p_box_max_p[m], p_box_max_index[m] = power_box_percent[m, :].max(), power_box_percent[m, :].argmax()
  100. # 切入风速特殊处理,如果切入风速过于偏右,向左拉回
  101. # todo 为什么第一行数据的索引值 > 14个就要往左拉回,还有是不是不叫切入风速,这个是 落入这个区间功率最多的个数的索引值
  102. if p_box_max_index[0] > 14:
  103. p_box_max_index[0] = 9
  104. # 以水平功率带方向为基准,进行分析
  105. dot_dense_left_right = np.zeros([power_bin_count, 2], dtype=int) # 存储每一水平功率带的功率主带以最大网格为中心,向向左,向右扩展的网格数
  106. dot_valve = 90 # 从中心向左右对称扩展网格的散点百分比和的阈值。
  107. for i in range(power_bin_count - 6): # 从最下层水平功率带1开始,向上到第PNum-6个水平功率带(额定功率一下水平功率带),逐一分析
  108. p_dot_dense_sum = p_box_max_p[i] # 以中心最大水平功率带为基准,向左向右对称扩展网格,累加各网格散点百分比
  109. i_spread_right = 1
  110. i_spread_left = 1
  111. while p_dot_dense_sum < dot_valve:
  112. if (p_box_max_index[i] + i_spread_right) < velocity_bin_count - 1:
  113. # 向右侧扩展
  114. p_dot_dense_sum = p_dot_dense_sum + power_box_percent[i, p_box_max_index[i] + i_spread_right]
  115. i_spread_right = i_spread_right + 1
  116. if (p_box_max_index[i] + i_spread_right) > velocity_bin_count - 1:
  117. break
  118. if (p_box_max_index[i] - i_spread_left) > 0:
  119. # 向左侧扩展
  120. p_dot_dense_sum = p_dot_dense_sum + power_box_percent[i, p_box_max_index[i] - i_spread_left]
  121. i_spread_left = i_spread_left + 1
  122. if (p_box_max_index[i] - i_spread_left) <= 0:
  123. break
  124. i_spread_right = i_spread_right - 1
  125. i_spread_left = i_spread_left - 1
  126. # 向左右对称扩展完毕
  127. dot_dense_left_right[i, 0] = i_spread_left
  128. dot_dense_left_right[i, 1] = i_spread_right
  129. main_band_right = np.median(dot_dense_left_right[:, 1])
  130. # 散点向右显著延展分布的水平功率带为限功率水平带
  131. # 各水平功率带是否为限功率标识,==1:是;==0:不是
  132. power_limit = np.zeros([power_bin_count, 1], dtype=int)
  133. width_average = 0 # 功率主带平均宽度
  134. # todo 限功率主带判别阈值为什么要加3
  135. power_limit_valve = np.ceil(main_band_right) + 3 # 限功率主带判别阈值
  136. n_counter = 0
  137. for i in range(power_bin_count - 6):
  138. # 如果向右扩展网格数大于阈值,且该水平功率带点总数>20,是限功率
  139. if dot_dense_left_right[i, 1] > power_limit_valve and power_bin_sum[i] > 20:
  140. power_limit[i] = 1
  141. if dot_dense_left_right[i, 1] <= power_limit_valve:
  142. # 统计正常水平功率带右侧宽度
  143. width_average = width_average + dot_dense_left_right[i, 1]
  144. n_counter = n_counter + 1
  145. width_average = width_average / n_counter # 功率主带平均宽度
  146. # 对限负荷水平功率带的最大网格较下面相邻层显著偏右,拉回
  147. for i in range(1, power_bin_count - 6):
  148. if power_limit[i] == 1 and abs(p_box_max_index[i] - p_box_max_index[i - 1]) > 5:
  149. p_box_max_index[i] = p_box_max_index[i - 1] + 1
  150. # 功率主带的右边界
  151. curve_width = int(np.ceil(width_average) + 2)
  152. # 数据异常需要剔除的网格标识,标识1:功率主带右侧的欠发网格;2:功率主带左侧的超发网格 3:额定功率以上的超发点
  153. b_box_remove = np.zeros([power_bin_count, velocity_bin_count], dtype=int)
  154. for m in range(power_bin_count - 6):
  155. for n in range(p_box_max_index[m] + curve_width, velocity_bin_count):
  156. b_box_remove[m, n] = 1
  157. for n in range(p_box_max_index[m] - curve_width, -1, -1):
  158. b_box_remove[m, n] = 2
  159. # 确定功率主带的左上拐点,即额定风速位置的网格索引
  160. curve_top = np.zeros(2, dtype=int)
  161. curve_top_valve = 3 # 网格的百分比阈值
  162. b_top_find = False
  163. for m in range(power_bin_count - 5, -1, -1):
  164. for n in range(velocity_bin_count):
  165. # 如左上角网格的百分比和散点个数大于阈值。
  166. if v_box_percent[m, n] > curve_top_valve and x_box_number[m, n] >= 10:
  167. curve_top[0] = m
  168. curve_top[1] = n
  169. b_top_find = True
  170. break
  171. if b_top_find:
  172. break
  173. isolate_valve = 3
  174. for m in range(power_bin_count - 6):
  175. for n in range(p_box_max_index[m] + curve_width, velocity_bin_count):
  176. if power_box_percent[m, n] < isolate_valve:
  177. b_box_remove[m, n] = 1
  178. # 功率主带顶部宽度
  179. curve_width_t = 2
  180. for m in range(power_bin_count - curve_width_t - 1, power_bin_count):
  181. for n in range(velocity_bin_count):
  182. b_box_remove[m, n] = 3 # 网格为额定功率以上的超发点
  183. # 功率主带拐点左侧的欠发网格标识
  184. for m in range(power_bin_count - 5 - 1, power_bin_count):
  185. for n in range(curve_top[1] - 1):
  186. b_box_remove[m, n] = 2
  187. # 以网格的标识,决定该网格内数据的标识。dzwind_and_power_sel。散点在哪个网格,此网格的标识即为该点的标识
  188. # -1:停机 0:好点 1:欠发功率点;2:超发功率点;3:额定风速以上的超发功率点 4: 限电
  189. dzwind_and_power_sel = np.zeros(power_gt_zero_array_count, dtype=int)
  190. n_which_p = -1
  191. n_which_v = -1
  192. for i in range(power_gt_zero_array_count):
  193. for m in range(power_bin_count):
  194. if m * 25 < power_gt_zero_array[i, 1] <= (m + 1) * 25:
  195. n_which_p = m
  196. break
  197. for n in range(velocity_bin_count):
  198. if (n * 0.25 + 0.125) < power_gt_zero_array[i, 0] <= ((n + 1) * 0.25 + 0.125):
  199. n_which_v = n
  200. break
  201. if n_which_p > -1 and n_which_v > -1:
  202. if b_box_remove[n_which_p, n_which_v] == 1:
  203. dzwind_and_power_sel[i] = 1
  204. if b_box_remove[n_which_p, n_which_v] == 2:
  205. dzwind_and_power_sel[i] = 2
  206. if b_box_remove[n_which_p, n_which_v] == 3:
  207. dzwind_and_power_sel[i] = 0 # 3 # 额定风速以上的超发功率点认为是正常点,不再标识。
  208. # 限负荷数据标识方法2:把数据切割为若干个窗口。对每一窗口,以第一个点为基准,连续nWindowLength个数据的功率在方差范围内,呈现显著水平分布的点
  209. n_window_length = 3
  210. limit_window = np.zeros(n_window_length, dtype=float)
  211. power_std = 15 # 功率波动方差
  212. n_window_num = int(np.floor(power_gt_zero_array_count / n_window_length))
  213. power_limit_up = self.rated_power - 300
  214. power_limit_low = 200
  215. for i in range(n_window_num):
  216. for j in range(n_window_length):
  217. limit_window[j] = power_gt_zero_array[i * n_window_length + j, 1]
  218. b_all_in_areas = 1
  219. for j in range(n_window_length):
  220. if limit_window[j] < power_limit_low or limit_window[j] > power_limit_up:
  221. b_all_in_areas = 0
  222. if b_all_in_areas == 0:
  223. continue
  224. up_limit = limit_window[0] + power_std
  225. low_limit = limit_window[0] - power_std
  226. b_all_in_up_low = 1
  227. for j in range(1, n_window_length):
  228. if limit_window[j] < low_limit or limit_window[j] > up_limit:
  229. b_all_in_up_low = 0
  230. if b_all_in_up_low == 1:
  231. for j in range(n_window_length):
  232. dzwind_and_power_sel[i * n_window_length + j] = 4 # 标识窗口内的数据为限负荷数据
  233. for i in range(power_bin_count - 6):
  234. pv_left_down = np.zeros(2, dtype=float)
  235. pv_right_up = np.zeros(2, dtype=float)
  236. if (p_box_max_index[i + 1] - p_box_max_index[i]) >= 1:
  237. pv_left_down[0] = (p_box_max_index[i] + curve_width) * 0.25 + 0.125
  238. pv_left_down[1] = i * 25
  239. pv_right_up[0] = (p_box_max_index[i + 1] + curve_width) * 0.25 + 0.125
  240. pv_right_up[1] = (i + 1) * 25
  241. for m in range(power_gt_zero_array_count):
  242. if pv_left_down[0] < power_gt_zero_array[m, 0] < pv_right_up[0] and \
  243. pv_left_down[1] < power_gt_zero_array[m, 1] < pv_right_up[1]: # 在该锯齿中
  244. if (power_gt_zero_array[m, 1] - pv_left_down[1]) / (
  245. power_gt_zero_array[m, 0] - pv_left_down[0]) > (
  246. pv_right_up[1] - pv_left_down[1]) / (
  247. pv_right_up[0] - pv_left_down[0]): # 斜率大于对角连线,则在锯齿左上三角形中,选中
  248. dzwind_and_power_sel[m] = 0
  249. self.df.loc[:, 'lab'] = -1
  250. self.df.loc[
  251. self.df[self.df[self.active_power] > 0].index, 'lab'] = dzwind_and_power_sel
  252. # 把部分欠发的优化为限电
  253. # 构建条件表达式
  254. cond1 = (self.df['lab'] == 1) & (
  255. (self.df[self.active_power] < self.rated_power * 0.75) &
  256. (self.df[self.pitch_angle_blade] > 0.5)
  257. )
  258. cond2 = (self.df['lab'] == 1) & (
  259. (self.df[self.active_power] < self.rated_power * 0.85) &
  260. (self.df[self.pitch_angle_blade] > 1.5)
  261. )
  262. cond3 = (self.df['lab'] == 1) & (
  263. (self.df[self.active_power] < self.rated_power * 0.9) &
  264. (self.df[self.pitch_angle_blade] > 2.5)
  265. )
  266. # 使用逻辑或操作符|合并条件
  267. combined_condition = cond1 | cond2 | cond3
  268. self.df.loc[combined_condition, 'lab'] = 4
  269. self.df.loc[self.df[self.active_power] <= 0, 'lab'] = -1
  270. self.df.reset_index(drop=True, inplace=True)
  271. if 'index' in self.df.columns:
  272. del self.df['index']
  273. return self.df
  274. def run(self):
  275. # Implement your class identification logic here
  276. begin = datetime.datetime.now()
  277. logger.info(f"打标签开始,风机号:{self.wind_turbine_number},数量:{self.df.shape}")
  278. try:
  279. df = self.identifier()
  280. except Exception as e:
  281. logger.error(traceback.format_exc())
  282. # message = str(e) + ',风机编号:' + self.wind_turbine_number
  283. # raise Exception('打标签失败:' + message)
  284. self.df.loc[:, 'lab'] = -999
  285. return self.df
  286. logger.info(f"打标签结束,{df.shape},耗时:{datetime.datetime.now() - begin}")
  287. return df