powerCurveAnalyst.py 16 KB

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