powerCurveAnalyst.py 16 KB

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