powerCurveAnalyst.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import os
  2. import numpy as np
  3. import pandas as pd
  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 utils.jsonUtil import JsonUtil
  9. class PowerCurveAnalyst(AnalystWithGoodPoint):
  10. """
  11. 风电机组功率曲线散点分析。
  12. 秒级scada数据运算太慢,建议使用分钟级scada数据
  13. """
  14. def typeAnalyst(self):
  15. return "power_curve"
  16. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  17. dictionary = self.processTurbineData(turbineCodes, conf, [
  18. Field_DeviceCode, Field_Time, Field_WindSpeed, Field_ActiverPower])
  19. dataFrameOfTurbines = self.userDataFrame(
  20. dictionary, conf.dataContract.configAnalysis, self)
  21. # 检查所需列是否存在
  22. required_columns = {Field_WindSpeed, Field_ActiverPower}
  23. if not required_columns.issubset(dataFrameOfTurbines.columns):
  24. raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
  25. turbrineInfos = self.common.getTurbineInfos(
  26. conf.dataContract.dataFilter.powerFarmID, turbineCodes, self.turbineInfo)
  27. groupedOfTurbineModel = turbrineInfos.groupby(Field_MillTypeCode)
  28. returnDatas = []
  29. for turbineModelCode, group in groupedOfTurbineModel:
  30. currTurbineCodes = group[Field_CodeOfTurbine].unique().tolist()
  31. currTurbineModeInfo = self.common.getTurbineModelByCode(
  32. turbineModelCode, self.turbineModelInfo)
  33. dataFrameOfContractPowerCurve = self.dataFrameContractOfTurbine[
  34. self.dataFrameContractOfTurbine[Field_MillTypeCode] == turbineModelCode]
  35. currDataFrameOfTurbines = dataFrameOfTurbines[dataFrameOfTurbines[Field_CodeOfTurbine].isin(
  36. currTurbineCodes)]
  37. powerCurveDataOfTurbines = self.dataReprocess(
  38. currDataFrameOfTurbines, self.binsWindSpeed)
  39. returnData = self.drawOfPowerCurve(
  40. powerCurveDataOfTurbines, outputAnalysisDir, conf, dataFrameOfContractPowerCurve, currTurbineModeInfo)
  41. returnDatas.append(returnData)
  42. returnJsonData= self.outputPowerCurveData(conf,outputAnalysisDir,currTurbineModeInfo,powerCurveDataOfTurbines,dataFrameOfContractPowerCurve)
  43. returnDatas.append(returnJsonData)
  44. returnResult = pd.concat(returnDatas, ignore_index=True)
  45. return returnResult
  46. def outputPowerCurveData(self, conf: Contract, outputAnalysisDir: str, turbineModelInfo: pd.Series, powerCurveDataOfTurbines: pd.DataFrame, dataFrameOfContractPowerCurve: pd.DataFrame) -> pd.DataFrame:
  47. turbineCodes = powerCurveDataOfTurbines[Field_CodeOfTurbine].unique()
  48. jsonDictionary = self.convert2Json(turbineModelInfo,turbineCodes=turbineCodes,
  49. dataFrameOfTurbines=powerCurveDataOfTurbines, dataFrameOfContract=dataFrameOfContractPowerCurve)
  50. jsonFileName = f"功率曲线数据-{turbineModelInfo[Field_MillTypeCode]}.json"
  51. jsonFilePath = os.path.join(outputAnalysisDir, jsonFileName)
  52. JsonUtil.write_json(jsonDictionary, file_path=jsonFilePath)
  53. result_rows = []
  54. result_rows.append({
  55. Field_Return_TypeAnalyst: self.typeAnalyst(),
  56. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  57. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  58. Field_CodeOfTurbine: Const_Output_Total,
  59. Field_Return_FilePath: jsonFilePath,
  60. Field_Return_IsSaveDatabase: True
  61. })
  62. for turbineCode in turbineCodes:
  63. result_rows.append({
  64. Field_Return_TypeAnalyst: self.typeAnalyst(),
  65. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  66. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  67. Field_CodeOfTurbine: turbineCode,
  68. Field_Return_FilePath: jsonFilePath,
  69. Field_Return_IsSaveDatabase: True
  70. })
  71. returnDatas = pd.DataFrame(result_rows)
  72. return returnDatas
  73. def convert2Json(self, turbineModelInfo: pd.Series,turbineCodes, dataFrameOfTurbines: pd.DataFrame, dataFrameOfContract: pd.DataFrame):
  74. result = {
  75. "analysisTypeCode": self.typeAnalyst(),
  76. "engineTypeCode": turbineModelInfo[Field_MillTypeCode] ,
  77. "engineTypeName": turbineModelInfo[Field_MachineTypeCode] ,
  78. "data": []
  79. }
  80. # 定义要替换的空值类型
  81. na_values = {pd.NA, float('nan')}
  82. # 从对象A提取数据
  83. for turbineCode in turbineCodes:
  84. data:pd.DataFrame=dataFrameOfTurbines[dataFrameOfTurbines[Field_CodeOfTurbine]==turbineCode]
  85. engine_data = {
  86. "enginName": data[Field_NameOfTurbine].iloc[0],
  87. "enginCode": turbineCode,
  88. "xData": data[Field_WindSpeed].replace(na_values, None).tolist(),
  89. "yData": data[Field_ActiverPower].replace(na_values, None).tolist(),
  90. "zData": []
  91. }
  92. result["data"].append(engine_data)
  93. # 从对象B提取数据
  94. contract_curve = {
  95. "enginName": "合同功率曲线",
  96. "xData": dataFrameOfContract[Field_WindSpeed].replace(na_values, None).tolist(),
  97. "yData": dataFrameOfContract[Field_ActiverPower].replace(na_values, None).tolist(),
  98. "zData": []
  99. }
  100. result["data"].append(contract_curve)
  101. return result
  102. def buildPowerCurveData(self, group: pd.DataFrame, fieldWindSpeed: str, fieldActivePower: str, bins) -> pd.DataFrame:
  103. """
  104. 计算设备的功率曲线。
  105. """
  106. powerCut = group.groupby(pd.cut(group[fieldWindSpeed], bins, labels=np.arange(0, 25.5, 0.5))).agg({
  107. fieldActivePower: 'median',
  108. fieldWindSpeed: ['median', 'count']
  109. })
  110. wind_count = powerCut[fieldWindSpeed]['count'].tolist()
  111. line = powerCut[fieldActivePower]['median'].round(decimals=2).tolist()
  112. act_line = pd.DataFrame([powerCut.index, wind_count, line]).T
  113. act_line.columns = [Field_WindSpeed,
  114. 'EffectiveQuantity', Field_ActiverPower]
  115. return act_line
  116. def dataReprocess(self, dataFrameMerge: pd.DataFrame, binsWindSpeed) -> pd.DataFrame:
  117. # 初始化结果DataFrame
  118. dataFrames = []
  119. # 按设备名分组数据
  120. grouped = dataFrameMerge.groupby(
  121. [Field_NameOfTurbine, Field_CodeOfTurbine])
  122. # 计算每个设备的功率曲线
  123. for name, group in grouped:
  124. dataFramePowerCurveTurbine = self.buildPowerCurveData(
  125. group, Field_WindSpeed, Field_ActiverPower, binsWindSpeed)
  126. dataFramePowerCurveTurbine[Field_NameOfTurbine] = name[0]
  127. dataFramePowerCurveTurbine[Field_CodeOfTurbine] = name[1]
  128. dataFrames.append(dataFramePowerCurveTurbine)
  129. # 绘制全场功率曲线图
  130. dataFrameReprocess: pd.DataFrame = pd.concat(
  131. dataFrames, ignore_index=True).reset_index(drop=True)
  132. return dataFrameReprocess
  133. def drawOfPowerCurve(self, powerCurveOfTurbines: pd.DataFrame, outputAnalysisDir, conf: Contract, dataFrameGuaranteePowerCurve: pd.DataFrame, turbineModelInfo: pd.Series):
  134. """
  135. 生成功率曲线并保存为文件。
  136. 参数:
  137. frames (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
  138. outputAnalysisDir (str): 分析输出目录。
  139. confData (ConfBusiness): 配置
  140. """
  141. # 绘制全场功率曲线图
  142. # ress =self.dataReprocess(dataFrameMerge,self.binsWindSpeed) # all_res.reset_index(drop=True)
  143. df1 = self.plot_power_curve(
  144. powerCurveOfTurbines, outputAnalysisDir, dataFrameGuaranteePowerCurve, Field_NameOfTurbine, conf, turbineModelInfo)
  145. # 绘制每个设备的功率曲线图
  146. grouped = powerCurveOfTurbines.groupby(
  147. [Field_NameOfTurbine, Field_CodeOfTurbine])
  148. df2 = pd.DataFrame() # 新建一个空表格,与返回的单图功率曲线合并
  149. for name, group in grouped:
  150. df_temp2 = self.plot_single_power_curve(
  151. powerCurveOfTurbines, group, dataFrameGuaranteePowerCurve, name, outputAnalysisDir, conf)
  152. df2 = pd.concat([df2, df_temp2], ignore_index=True)
  153. # 总图与单图的表格合并
  154. df = pd.concat([df1, df2], ignore_index=True)
  155. return df
  156. def plot_power_curve(self, ress, output_path, dataFrameGuaranteePowerCurve: pd.DataFrame, Field_NameOfTurbine, conf: Contract, turbineModelInfo: pd.Series):
  157. """
  158. 绘制全场功率曲线图。
  159. """
  160. # colors = px.colors.sequential.Turbo
  161. fig = go.Figure()
  162. for turbine_num in ress[Field_NameOfTurbine].unique():
  163. turbine_data = ress[ress[Field_NameOfTurbine] == turbine_num]
  164. # 循环创建风速-功率折线
  165. fig.add_trace(go.Scatter(
  166. x=turbine_data[Field_WindSpeed],
  167. y=turbine_data[Field_ActiverPower],
  168. mode='lines',
  169. # line=dict(color=colors[idx % len(colors)]),
  170. name=f'{turbine_num}' # 使用风电机组编号作为图例的名称
  171. )
  172. )
  173. if not ress.empty and Field_CutInWS in ress.columns and ress[Field_CutInWS].notna().any():
  174. cut_in_ws = ress[Field_CutInWS].min() - 1
  175. else:
  176. cut_in_ws = 2
  177. fig.add_trace(go.Scatter(
  178. x=dataFrameGuaranteePowerCurve[Field_WindSpeed],
  179. y=dataFrameGuaranteePowerCurve[Field_ActiverPower],
  180. # mode='lines',
  181. # line=dict(color='red', dash='dash'),
  182. mode='lines+markers',
  183. line=dict(color='red'),
  184. marker=dict(color='red', size=5),
  185. name='合同功率曲线',
  186. showlegend=True
  187. )
  188. )
  189. # 创建布局
  190. fig.update_layout(
  191. title={
  192. "text": f'功率曲线-{turbineModelInfo[Field_MachineTypeCode]}',
  193. 'x': 0.5
  194. },
  195. # legend_title='Turbine',
  196. xaxis=dict(
  197. title='风速',
  198. dtick=1,
  199. tickangle=-45,
  200. range=[cut_in_ws, 25]
  201. ),
  202. yaxis=dict(
  203. title='有功功率',
  204. dtick=self.axisStepActivePower,
  205. range=[self.axisLowerLimitActivePower,
  206. self.axisUpperLimitActivePower]
  207. ),
  208. legend=dict(
  209. orientation="h", # Horizontal orientation
  210. xanchor="center", # Anchor the legend to the center
  211. x=0.5, # Position legend at the center of the x-axis
  212. y=-0.2, # Position legend below the x-axis
  213. # itemsizing='constant', # Keep the size of the legend entries constant
  214. # itemwidth=50
  215. )
  216. )
  217. # 保存HTML
  218. htmlFileName = '全场-{}-{}-功率曲线.html'.format(self.powerFarmInfo[Field_PowerFarmName].iloc[0],turbineModelInfo[Field_MillTypeCode])
  219. htmlFilePath = os.path.join(output_path, htmlFileName)
  220. fig.write_html(htmlFilePath)
  221. result_rows = []
  222. result_rows.append({
  223. Field_Return_TypeAnalyst: self.typeAnalyst(),
  224. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  225. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  226. Field_CodeOfTurbine: Const_Output_Total,
  227. Field_Return_FilePath: htmlFilePath,
  228. Field_Return_IsSaveDatabase: False
  229. })
  230. result_df = pd.DataFrame(result_rows)
  231. return result_df
  232. def plot_single_power_curve(self, ress, group, dataFrameGuaranteePowerCurve: pd.DataFrame, turbineName, outputAnalysisDir, conf: Contract):
  233. fig = go.Figure()
  234. for turbine_num in ress[Field_NameOfTurbine].unique():
  235. turbine_data = ress[ress[Field_NameOfTurbine] == turbine_num]
  236. # 循环创建风速-功率折线
  237. fig.add_trace(go.Scatter(
  238. x=turbine_data[Field_WindSpeed],
  239. y=turbine_data[Field_ActiverPower],
  240. mode='lines',
  241. line=dict(color='lightgrey'),
  242. name=f'{turbine_num}',
  243. showlegend=False
  244. )
  245. )
  246. if not ress.empty and Field_CutInWS in ress.columns and ress[Field_CutInWS].notna().any():
  247. cut_in_ws = ress[Field_CutInWS].min() - 1
  248. else:
  249. cut_in_ws = 2
  250. fig.add_trace(go.Scatter(
  251. x=group[Field_WindSpeed],
  252. y=group[Field_ActiverPower],
  253. mode='lines',
  254. line=dict(color='darkblue'),
  255. name=Field_ActiverPower,
  256. showlegend=False
  257. )
  258. )
  259. fig.add_trace(go.Scatter(
  260. x=dataFrameGuaranteePowerCurve[Field_WindSpeed],
  261. y=dataFrameGuaranteePowerCurve[Field_ActiverPower],
  262. mode='lines+markers',
  263. line=dict(color='red'),
  264. marker=dict(color='red', size=5),
  265. name='合同功率曲线',
  266. showlegend=True
  267. )
  268. )
  269. # 创建布局
  270. fig.update_layout(
  271. title={
  272. "text": f'机组: {turbineName[0]}'
  273. },
  274. legend=dict(
  275. orientation="h", # 或者 "v" 表示垂直
  276. yanchor="bottom", # 图例垂直对齐方式
  277. y=0, # 图例距离y轴下边界的距离(0到1之间)
  278. xanchor="right", # 图例水平对齐方式
  279. x=1, # 图例距离x轴右边界的距离(0到1之间)
  280. bgcolor='rgba(255,255,255,0)'
  281. ),
  282. xaxis=dict(
  283. title='风速',
  284. dtick=1,
  285. tickangle=-45,
  286. range=[cut_in_ws, 25]
  287. ),
  288. yaxis=dict(
  289. title='有功功率',
  290. dtick=self.axisStepActivePower,
  291. range=[self.axisLowerLimitActivePower,
  292. self.axisUpperLimitActivePower]
  293. )
  294. )
  295. # 保存图像
  296. pngFileName = f"{turbineName[0]}.png"
  297. pngFilePath = os.path.join(outputAnalysisDir, pngFileName)
  298. fig.write_image(pngFilePath, scale=3)
  299. # 保存HTML
  300. htmlFileName = f"{turbineName[0]}.html"
  301. htmlFilePath = os.path.join(outputAnalysisDir, htmlFileName)
  302. fig.write_html(htmlFilePath)
  303. result_rows = []
  304. result_rows.append({
  305. Field_Return_TypeAnalyst: self.typeAnalyst(),
  306. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  307. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  308. Field_CodeOfTurbine: turbineName[1],
  309. Field_Return_FilePath: pngFilePath,
  310. Field_Return_IsSaveDatabase: False
  311. })
  312. result_rows.append({
  313. Field_Return_TypeAnalyst: self.typeAnalyst(),
  314. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  315. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  316. Field_CodeOfTurbine: turbineName[1],
  317. Field_Return_FilePath: htmlFilePath,
  318. Field_Return_IsSaveDatabase: False
  319. })
  320. result_df = pd.DataFrame(result_rows)
  321. return result_df