yawErrorAnalyst.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import os
  2. import numpy as np
  3. from plotly.subplots import make_subplots
  4. import plotly.graph_objects as go
  5. from scipy.optimize import curve_fit
  6. import matplotlib.pyplot as plt
  7. import pandas as pd
  8. from behavior.analyst import Analyst
  9. from utils.directoryUtil import DirectoryUtil as dir
  10. from algorithmContract.confBusiness import *
  11. class YawErrorAnalyst(Analyst):
  12. """
  13. 风电机组静态偏航误差分析
  14. """
  15. fieldWindDirFloor = 'wind_dir_floor'
  16. fieldPower = 'power'
  17. fieldPowerMean = 'mean_power'
  18. fieldPowerMax = 'max_power'
  19. fieldPowerMin = 'min_power'
  20. fieldPowerMedian = 'median_power'
  21. fieldPowerGT0p = 'power_gt_0'
  22. fieldPowerGT80p = 'power_gt_80p'
  23. fieldPowerRatio0p = 'ratio_0'
  24. fieldPowerRatio80p = 'ratio_80p'
  25. fieldSlop = 'slop'
  26. def typeAnalyst(self):
  27. return "yaw_error"
  28. def filterCommon(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
  29. dataFrame = super().filterCommon(dataFrame, confData)
  30. dataFrame = dataFrame[~((dataFrame[Field_AngleIncluded].abs() >= 90))]
  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, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
  68. self.yawErrorAnalysis(dataFrameMerge, outputAnalysisDir, confData)
  69. def yawErrorAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
  70. # 检查所需列是否存在
  71. required_columns = {confData.field_power,confData.field_angle_included}
  72. if not required_columns.issubset(dataFrameMerge.columns):
  73. raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
  74. results = []
  75. grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
  76. for name, group in grouped:
  77. df = self.calculateYawError(
  78. group, confData.field_angle_included, confData.field_power)
  79. df.dropna(inplace=True)
  80. # drop extreme value, the 3 largest and 3 smallest
  81. df_ = df[df[self.fieldPowerRatio80p] > 0.000]
  82. xdata = df_[self.fieldWindDirFloor]
  83. ydata = df_[self.fieldSlop]
  84. # make ydata smooth
  85. ydata = ydata.rolling(7).median()[6:]
  86. xdata = xdata[3:-3]
  87. if len(xdata) <= 0 and len(ydata) <= 0:
  88. continue
  89. # Curve fitting
  90. popt, pcov = curve_fit(self.poly_func, xdata, ydata)
  91. # popt contains the optimized parameters a, b, and c
  92. # Generate fitted y-dataFrame using the optimized parameters
  93. fitted_ydata = self.poly_func(xdata, *popt)
  94. # get the max value of fitted_ydata and its index
  95. max_pos = fitted_ydata.idxmax()
  96. # Create a subplot with two rows
  97. fig = make_subplots(rows=2, cols=1)
  98. # First subplot
  99. fig.add_trace(
  100. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldSlop],
  101. mode='markers', name="energy gain", marker=dict(size=5)),
  102. row=1, col=1
  103. )
  104. fig.add_trace(
  105. go.Scatter(x=xdata, y=fitted_ydata, mode='lines', name="fit", line=dict(color='red')),
  106. row=1, col=1
  107. )
  108. fig.add_trace(
  109. go.Scatter(x=[df[self.fieldWindDirFloor][max_pos]], y=[fitted_ydata[max_pos]],
  110. mode='markers', name="max pos:"+str(df[self.fieldWindDirFloor][max_pos]),
  111. marker=dict(size=20)),
  112. row=1, col=1
  113. )
  114. # Second subplot
  115. fig.add_trace(
  116. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio0p],
  117. mode='markers', name="base energy", marker=dict(size=5)),
  118. row=2, col=1
  119. )
  120. fig.add_trace(
  121. go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio80p],
  122. mode='markers', name="slope energy", marker=dict(size=5)),
  123. row=2, col=1
  124. )
  125. # Update layout
  126. fig.update_layout(
  127. title={
  128. "text": f"Yaw Error Analysis {name}",
  129. "x": 0.5
  130. },
  131. showlegend=True,
  132. margin=dict(t=50, b=10) # t为顶部(top)间距,b为底部(bottom)间距
  133. )
  134. # Save to file
  135. fig.write_image(os.path.join(outputAnalysisDir, f"{name}.png"))
  136. # calc squear error of fitted_ydata and ydata
  137. print(name, "\t", df[self.fieldWindDirFloor][max_pos])
  138. resultOfTurbine = [name, df[self.fieldWindDirFloor][max_pos]]
  139. results.append(resultOfTurbine)
  140. # 初始化一个空的DataFrame,指定列名
  141. columns = [Field_NameOfTurbine, Field_YawError]
  142. dataFrameResult = pd.DataFrame(results, columns=columns)
  143. filePathOfYawError = os.path.join(
  144. outputAnalysisDir, f"yaw_error_result{CSVSuffix}")
  145. dataFrameResult.to_csv(filePathOfYawError, index=False)