yawErrorAnalyst.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. import plotly.graph_objects as go
  5. from algorithmContract.confBusiness import *
  6. from algorithmContract.contract import Contract
  7. from behavior.analystWithGoodPoint import AnalystWithGoodPoint
  8. from plotly.subplots import make_subplots
  9. from scipy.optimize import curve_fit
  10. class YawErrorAnalyst(AnalystWithGoodPoint):
  11. """
  12. 风电机组静态偏航误差分析
  13. """
  14. fieldWindDirFloor = 'wind_dir_floor'
  15. fieldPower = 'power'
  16. fieldPowerMean = 'mean_power'
  17. fieldPowerMax = 'max_power'
  18. fieldPowerMin = 'min_power'
  19. fieldPowerMedian = 'median_power'
  20. fieldPowerGT0p = 'power_gt_0'
  21. fieldPowerGT80p = 'power_gt_80p'
  22. fieldPowerRatio0p = 'ratio_0'
  23. fieldPowerRatio80p = 'ratio_80p'
  24. fieldSlop = 'slop'
  25. def typeAnalyst(self):
  26. return "yaw_error"
  27. def selectColumns(self):
  28. return [Field_DeviceCode,Field_Time,Field_WindSpeed,Field_ActiverPower,Field_AngleIncluded]
  29. def filterCommon(self, dataFrame: pd.DataFrame, conf: Contract):
  30. dataFrame = dataFrame[~((dataFrame[Field_AngleIncluded].abs() >= 15))]
  31. return dataFrame
  32. def calculateYawError(self, dataFrame: pd.DataFrame, fieldAngleInclude, fieldActivePower):
  33. dataFrame = dataFrame.dropna(
  34. subset=[Field_NameOfTurbine, fieldAngleInclude, fieldActivePower])
  35. # Calculate floor values and other transformations
  36. dataFrame[self.fieldWindDirFloor] = np.floor(
  37. dataFrame[fieldAngleInclude]).astype(int)
  38. dataFrame[self.fieldPower] = dataFrame[fieldActivePower].astype(float)
  39. # Calculate aggregated metrics for power
  40. grouped = dataFrame.groupby(self.fieldWindDirFloor).agg({
  41. Field_NameOfTurbine: ['min'],
  42. self.fieldPower: ['mean', 'max', 'min', 'median', lambda x: (
  43. x > 0).sum(), lambda x: (x > x.median()).sum()]
  44. }).reset_index()
  45. # Rename columns for clarity
  46. grouped.columns = [self.fieldWindDirFloor, Field_NameOfTurbine, self.fieldPowerMean, self.fieldPowerMax,
  47. self.fieldPowerMin, self.fieldPowerMedian, self.fieldPowerGT0p, self.fieldPowerGT80p]
  48. # Calculate total sums for conditions
  49. power_gt_0_sum = grouped[self.fieldPowerGT0p].sum()
  50. power_gt_80p_sum = grouped[self.fieldPowerGT80p].sum()
  51. # Calculate ratios
  52. grouped[self.fieldPowerRatio0p] = grouped[self.fieldPowerGT0p] / \
  53. power_gt_0_sum
  54. grouped[self.fieldPowerRatio80p] = grouped[self.fieldPowerGT80p] / \
  55. power_gt_80p_sum
  56. # Filter out zero ratios and calculate slope
  57. grouped = grouped[grouped[self.fieldPowerRatio0p] > 0]
  58. grouped[self.fieldSlop] = grouped[self.fieldPowerRatio80p] / \
  59. grouped[self.fieldPowerRatio0p]
  60. # Sort by wind direction floor
  61. grouped.sort_values(self.fieldWindDirFloor, inplace=True)
  62. # # Write to CSV
  63. # grouped.to_csv(output_path, index=False)
  64. return grouped
  65. def poly_func(self, x, a, b, c, d, e):
  66. return a * x**4 + b * x ** 3 + c * x ** 2 + d * x + e
  67. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  68. dictionary = self.processTurbineData(turbineCodes,conf,self.selectColumns())
  69. dataFrameMerge = self.userDataFrame(dictionary,conf.dataContract.configAnalysis,self)
  70. return self.yawErrorAnalysis(dataFrameMerge, outputAnalysisDir, conf)
  71. def yawErrorAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, conf: Contract):
  72. # 检查所需列是否存在
  73. required_columns = {Field_ActiverPower, Field_YawError}
  74. if not required_columns.issubset(dataFrameMerge.columns):
  75. raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
  76. results = []
  77. result_rows = []
  78. grouped = dataFrameMerge.groupby(
  79. [Field_NameOfTurbine, Field_CodeOfTurbine])
  80. for name, group in grouped:
  81. df = self.calculateYawError(
  82. group, Field_YawError, Field_ActiverPower)
  83. df.dropna(inplace=True)
  84. # drop extreme value, the 3 largest and 3 smallest
  85. df_ = df[df[self.fieldPowerRatio80p] > 0.000]
  86. xdata = df_[self.fieldWindDirFloor]
  87. ydata = df_[self.fieldSlop]
  88. # make ydata smooth
  89. ydata = ydata.rolling(7).median()[6:]
  90. xdata = xdata[3:-3]
  91. if len(xdata) <= 0 and len(ydata) <= 0:
  92. continue
  93. # Curve fitting
  94. popt, pcov = curve_fit(self.poly_func, xdata, ydata)
  95. # popt contains the optimized parameters a, b, and c
  96. # Generate fitted y-dataFrame using the optimized parameters
  97. fitted_ydata = self.poly_func(xdata, *popt)
  98. # get the max value of fitted_ydata and its index
  99. max_pos = fitted_ydata.idxmax()
  100. # Create a subplot with two rows
  101. fig = make_subplots(rows=2, cols=1)
  102. # First subplot
  103. fig.add_trace(
  104. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldSlop],
  105. mode='markers', name="Energy Gain", marker=dict(size=5)),
  106. row=1, col=1
  107. )
  108. fig.add_trace(
  109. go.Scatter(x=xdata, y=fitted_ydata, mode='lines',
  110. name="Fit", line=dict(color='red')),
  111. row=1, col=1
  112. )
  113. fig.add_trace(
  114. go.Scatter(x=[df[self.fieldWindDirFloor][max_pos]], y=[fitted_ydata[max_pos]],
  115. mode='markers', name="Max Pos:"+str(df[self.fieldWindDirFloor][max_pos]),
  116. marker=dict(size=20)),
  117. row=1, col=1
  118. )
  119. # Second subplot
  120. fig.add_trace(
  121. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio0p],
  122. mode='markers', name="Base Energy", marker=dict(size=5)),
  123. row=2, col=1
  124. )
  125. fig.add_trace(
  126. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio80p],
  127. mode='markers', name="Slope Energy", marker=dict(size=5)),
  128. row=2, col=1
  129. )
  130. # Update layout
  131. fig.update_layout(
  132. title={
  133. "text": f"Yaw Error Analysis: {name[0]}",
  134. "x": 0.5
  135. },
  136. showlegend=True,
  137. margin=dict(t=50, b=10) # t为顶部(top)间距,b为底部(bottom)间距
  138. )
  139. # Save to file
  140. filePathOfImage = os.path.join(outputAnalysisDir, f"{name[0]}.png")
  141. fig.write_image(filePathOfImage, scale=3)
  142. filePathOfHtml = os.path.join(outputAnalysisDir, f"{name[0]}.html")
  143. fig.write_html(filePathOfHtml)
  144. # calc squear error of fitted_ydata and ydata
  145. print(name[0], "\t", df[self.fieldWindDirFloor][max_pos])
  146. resultOfTurbine = [name[0], df[self.fieldWindDirFloor][max_pos]]
  147. results.append(resultOfTurbine)
  148. result_rows.append({
  149. Field_Return_TypeAnalyst: self.typeAnalyst(),
  150. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  151. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  152. Field_CodeOfTurbine: name[1],
  153. Field_Return_FilePath: filePathOfImage,
  154. Field_Return_IsSaveDatabase: False
  155. })
  156. result_rows.append({
  157. Field_Return_TypeAnalyst: self.typeAnalyst(),
  158. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  159. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  160. Field_CodeOfTurbine: name[1],
  161. Field_Return_FilePath: filePathOfHtml,
  162. Field_Return_IsSaveDatabase: False
  163. })
  164. # 初始化一个空的DataFrame,指定列名
  165. columns = [Field_NameOfTurbine, Field_YawError]
  166. dataFrameResult = pd.DataFrame(results, columns=columns)
  167. filePathOfYawError = os.path.join(
  168. outputAnalysisDir, f"yaw_error_result{CSVSuffix}")
  169. dataFrameResult.to_csv(filePathOfYawError, index=False)
  170. result_rows.append({
  171. Field_Return_TypeAnalyst: self.typeAnalyst(),
  172. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  173. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  174. Field_CodeOfTurbine: "total",
  175. Field_Return_FilePath: filePathOfYawError,
  176. Field_Return_IsSaveDatabase: True
  177. })
  178. result_df = pd.DataFrame(result_rows)
  179. return result_df