import numpy as np import pandas as pd import os import plotly.graph_objects as go # 声明全局变量 fieldActivePower = "功率" fieldWindSpeed = "风速" fieldPitchAngle = "叶片角度" PRated = 1500 # 风机额定功率 VCutOut = 25 # 切出风速 VCutIn = 3 # 切入风速 VRated = 10 # 额定风速 VNum = int(VCutOut / 0.25) # 风速分区数量 # 读取数据函数 def read_data(): # 读取 CSV 文件 March809 = pd.read_csv( "./data/DataClassificationIdentification/A01-G.csv", encoding="utf-8") IdealCurve = pd.read_csv( "./data/DataClassificationIdentification/A型风机设计功率曲线.csv", encoding="utf-8") return March809, IdealCurve # 计算统计信息函数 def calculate_statistics(March809, IdealCurve): AA = len(March809) BB = len(IdealCurve) PowerMax = March809[fieldActivePower].max() PowerRated = int(np.ceil(PowerMax / 100) * 100) PNum = PowerRated // 25 # 功率分区数量 # 计算实际发电量 EPActualTotal = March809[March809[fieldActivePower] >= 0][fieldActivePower].sum() / 6 WindSpeedAvr = March809[fieldWindSpeed].mean() # 计算风机可利用率 nShouldGP = np.sum(March809[fieldWindSpeed] >= VCutIn) nRealGP = np.sum((March809[fieldWindSpeed] >= VCutIn) & (March809[fieldActivePower] > 0)) TurbineRunRate = (nRealGP / nShouldGP * 100) if nShouldGP > 0 else 0 # 计算理论发电量 EPIdealTotalAAA = 0 for i in range(AA): nWhichBin = 0 for m in range(BB - 1): if IdealCurve.iloc[m][fieldWindSpeed] < March809.iloc[i][fieldWindSpeed] <= IdealCurve.iloc[m + 1][fieldWindSpeed]: nWhichBin = m break if nWhichBin > 0: IdealPower = (March809.iloc[i][fieldWindSpeed] - IdealCurve.iloc[nWhichBin][fieldWindSpeed]) / (IdealCurve.iloc[nWhichBin + 1][fieldWindSpeed] - IdealCurve.iloc[nWhichBin] [fieldWindSpeed]) * (IdealCurve.iloc[nWhichBin + 1][fieldActivePower] - IdealCurve.iloc[nWhichBin][fieldActivePower]) + IdealCurve.iloc[nWhichBin][fieldActivePower] EPIdealTotalAAA += IdealPower / 6 return AA, BB, PNum, EPActualTotal, WindSpeedAvr, TurbineRunRate, EPIdealTotalAAA # 分类数据函数 def classify_data(March809, PNum, VNum): DzMarch809 = March809[March809[fieldActivePower] > 0] nCounter1 = len(DzMarch809) XBoxNumber = np.ones((PNum, VNum), dtype=int) for i in range(nCounter1): nWhichP = np.digitize( DzMarch809.iloc[i][fieldActivePower], np.arange(0, PNum * 25, 25)) - 1 nWhichV = np.digitize( DzMarch809.iloc[i][fieldWindSpeed], np.arange(0.125, VNum * 0.25, 0.25)) if nWhichP < PNum and nWhichV < VNum: XBoxNumber[nWhichP, nWhichV] += 1 XBoxNumber -= 1 return DzMarch809, XBoxNumber # 计算百分比函数 def compute_percentages(XBoxNumber, PNum, VNum): PBoxPercent = np.zeros((PNum, VNum)) PBinSum = XBoxNumber.sum(axis=1) for i in range(PNum): if PBinSum[i] > 0: PBoxPercent[i, :] = XBoxNumber[i, :] / PBinSum[i] * 100 VBoxPercent = np.zeros((PNum, VNum)) VBinSum = XBoxNumber.sum(axis=0) for i in range(VNum): if VBinSum[i] > 0: VBoxPercent[:, i] = XBoxNumber[:, i] / VBinSum[i] * 100 return PBoxPercent, VBoxPercent # 查找主带函数 def find_main_band(PBoxPercent, PNum, VNum, XBoxNumber): PBoxMaxIndex = np.argmax(PBoxPercent, axis=1) PBoxMaxP = np.max(PBoxPercent, axis=1) DotDense = np.zeros(PNum) DotDenseLeftRight = np.zeros((PNum, 2), dtype=int) DotValve = 90 for i in range(PNum - 6): PDotDenseSum = PBoxMaxP[i] iSpreadRight = iSpreadLeft = 1 while PDotDenseSum < DotValve: if (PBoxMaxIndex[i] + iSpreadRight) < VNum - 1: PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] + iSpreadRight] iSpreadRight += 1 if (PBoxMaxIndex[i] + iSpreadRight) > VNum - 1: break if (PBoxMaxIndex[i] - iSpreadLeft) > 0: PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] - iSpreadLeft] iSpreadLeft += 1 if (PBoxMaxIndex[i] - iSpreadLeft) <= 0: break iSpreadRight -= 1 iSpreadLeft -= 1 DotDenseLeftRight[i, :] = [iSpreadLeft, iSpreadRight] DotDense[i] = iSpreadLeft + iSpreadRight + 1 DotDenseWidthLeft = DotDenseLeftRight[:, 1] MainBandRight = np.median(DotDenseWidthLeft) PowerLimitValve = int(np.ceil(MainBandRight)) + 3 PowerLimit = np.zeros(PNum) nCounterLimit = nCounter = 0 WidthAverage = 0 for i in range(PNum - 6): if DotDenseLeftRight[i, 1] > PowerLimitValve and XBoxNumber[i, :].sum() > 20: PowerLimit[i] = 1 nCounterLimit += 1 else: WidthAverage += DotDenseLeftRight[i, 1] nCounter += 1 WidthAverage /= nCounter WidthVar = np.sqrt(np.mean((DotDenseLeftRight[:, 1] - WidthAverage) ** 2)) PowerBandWidth = WidthAverage * 2 * 0.25 return DotDense, DotDenseLeftRight, PowerLimit, WidthAverage, WidthVar, PowerBandWidth, PBoxMaxIndex # 标记坏点函数 def mark_bad_points(DzMarch809, DotDenseLeftRight, PBoxMaxIndex, PowerLimit, PNum, VNum, XBoxNumber, PBoxPercent): CurveWidthR = int(np.ceil(DotDenseLeftRight[:, 1].mean())) + 2 CurveWidthL = CurveWidthR BBoxLimit = np.zeros((PNum, VNum)) for i in range(3, PNum - 6): if PowerLimit[i] == 1: BBoxLimit[i, PBoxMaxIndex[i] + CurveWidthR + 1: VNum] = 1 BBoxRemove = np.zeros((PNum, VNum)) for m in range(PNum - 6): BBoxRemove[m, PBoxMaxIndex[m] + CurveWidthR: VNum] = 1 BBoxRemove[m, :PBoxMaxIndex[m] - CurveWidthL + 1] = 2 CurveTop = [0, 0] CurveTopValve = 3 BTopFind = False for m in range(PNum - 4, 0, -1): for n in range(VNum): if PBoxPercent[m, n] > CurveTopValve and XBoxNumber[m, n] >= 10: CurveTop = [m, n] BTopFind = True break if BTopFind: break IsolateValve = 3 for m in range(PNum - 6): for n in range(PBoxMaxIndex[m] + CurveWidthR, VNum): if PBoxPercent[m, n] < IsolateValve: BBoxRemove[m, n] = 1 for m in range(PNum - 6, PNum): BBoxRemove[m, :] = 3 for m in range(PNum - 5, PNum): BBoxRemove[m, :CurveTop[1] - 2] = 2 DzMarch809Sel = np.zeros(len(DzMarch809), dtype=int) for i in range(len(DzMarch809)): nWhichP = np.digitize( DzMarch809.iloc[i][fieldActivePower], np.arange(0, PNum * 25, 25)) - 1 nWhichV = np.digitize( DzMarch809.iloc[i][fieldWindSpeed], np.arange(0.125, VNum * 0.25, 0.25)) # if ( # (DzMarch809.iloc[i][fieldActivePower] < PRated * 0.75) and # (DzMarch809.iloc[i][fieldPitchAngle] > 0.5) or # (DzMarch809.iloc[i][fieldActivePower] < PRated * 0.85) and # (DzMarch809.iloc[i][fieldPitchAngle] > 1.5) or # (DzMarch809.iloc[i][fieldActivePower] < PRated * 0.9) and # (DzMarch809.iloc[i][fieldPitchAngle] > 2.5) # ): # continue if nWhichP < PNum and nWhichV < VNum: if BBoxRemove[nWhichP, nWhichV] == 1: DzMarch809Sel[i] = 1 elif BBoxRemove[nWhichP, nWhichV] == 2: DzMarch809Sel[i] = 2 elif BBoxRemove[nWhichP, nWhichV] == 3: DzMarch809Sel[i] = 0 return DzMarch809Sel def identify_limit_load_data(DzMarch809: pd.DataFrame, DzMarch809Sel, PRated): nCounter1 = len(DzMarch809) PVLimit = np.zeros((nCounter1, 2)) nLimitTotal = 0 nWindowLength = 3 PowerStd = 15 # 功率波动方差 PowerLimitUp = PRated - 300 PowerLimitLow = 5 # 200kW nWindowNum = nCounter1 // nWindowLength for i in range(nWindowNum): LimitWindow = DzMarch809.iloc[i * nWindowLength:(i + 1) * nWindowLength][fieldActivePower].values # 检查所有数据是否在 PowerLimitLow值~PowerLimitUp值范围内 if not ((LimitWindow >= PowerLimitLow) & (LimitWindow <= PowerLimitUp)).all(): continue """ 限功率识别策略(参考): 1. 功率<额定功率(PRated)*0.75 and 叶片角度>0.5 ; 2. 功率<额定功率*0.85 and 叶片角度>1.5 ; 3. 功率<额定功率*0.9 and 叶片角度>2.5; 示例: 1. 功率<1100,and 叶片角度>0.5 ; 2. 功率<1250 and 叶片角度>1.5 ; 3. 功率<1400 and 叶片角度>2.5 ; """ # 额外的限功率识别策略 pitch_angle_window = DzMarch809.iloc[i * nWindowLength:(i + 1) * nWindowLength][fieldPitchAngle].values if ( (DzMarch809.iloc[i * nWindowLength:(i + 1) * nWindowLength][fieldActivePower] < PRated * 0.75).any() and (pitch_angle_window > 0.5).any() or (DzMarch809.iloc[i * nWindowLength:(i + 1) * nWindowLength][fieldActivePower] < PRated * 0.85).any() and (pitch_angle_window > 1.5).any() or (DzMarch809.iloc[i * nWindowLength:(i + 1) * nWindowLength][fieldActivePower] < PRated * 0.9).any() and (pitch_angle_window > 2.5).any() ): # 标识限负荷数据 DzMarch809Sel[i * nWindowLength:(i + 1) * nWindowLength] = 4 UpLimit = LimitWindow[0] + PowerStd LowLimit = LimitWindow[0] - PowerStd # 检查所有数据是否在方差范围内 if not ((LimitWindow[1:] >= LowLimit) & (LimitWindow[1:] <= UpLimit)).all(): continue # 标识限负荷数据 DzMarch809Sel[i * nWindowLength:(i + 1) * nWindowLength] = 4 for j in range(nWindowLength): # 只提取功率和风速数据 PVLimit[nLimitTotal] = DzMarch809.iloc[i * nWindowLength + j][[fieldWindSpeed, fieldActivePower]] nLimitTotal += 1 PVLimit = PVLimit[:nLimitTotal] # 截取实际数据部分 return PVLimit, DzMarch809Sel # 计算能量损失函数 def calculate_energy_loss(DzMarch809Sel, DzMarch809, IdealCurve, PNum, BB, PRated, EPIdealTotalAAA, EPActualTotal): EPLostStopTotal = 0 nStopTotal = 0 for i in range(len(DzMarch809)): if DzMarch809.iloc[i][fieldActivePower] <= 0: nWhichBin = 0 for m in range(BB - 1): if IdealCurve.iloc[m][fieldWindSpeed] < DzMarch809.iloc[i][fieldWindSpeed] <= IdealCurve.iloc[m + 1][fieldWindSpeed]: nWhichBin = m break if nWhichBin > 0: IdealPower = (DzMarch809.iloc[i][fieldWindSpeed] - IdealCurve.iloc[nWhichBin][fieldWindSpeed]) / (IdealCurve.iloc[nWhichBin + 1][fieldWindSpeed] - IdealCurve.iloc[nWhichBin] [fieldWindSpeed]) * (IdealCurve.iloc[nWhichBin + 1][fieldActivePower] - IdealCurve.iloc[nWhichBin][fieldActivePower]) + IdealCurve.iloc[nWhichBin][fieldActivePower] EPLostStopTotal += IdealPower / 6 nStopTotal += 1 EPLostBadTotal = 0 EPLost = 0 nBadTotal = 0 for i in range(len(DzMarch809)): if DzMarch809Sel[i] == 1: nWhichBin = 0 for m in range(BB - 1): if IdealCurve.iloc[m][fieldWindSpeed] < DzMarch809.iloc[i][fieldWindSpeed] <= IdealCurve.iloc[m + 1][fieldWindSpeed]: nWhichBin = m break if nWhichBin > 0: IdealPower = (DzMarch809.iloc[i][fieldWindSpeed] - IdealCurve.iloc[nWhichBin][fieldWindSpeed]) / (IdealCurve.iloc[nWhichBin + 1][fieldWindSpeed] - IdealCurve.iloc[nWhichBin] [fieldWindSpeed]) * (IdealCurve.iloc[nWhichBin + 1][fieldActivePower] - IdealCurve.iloc[nWhichBin][fieldActivePower]) + IdealCurve.iloc[nWhichBin][fieldActivePower] EPLost += abs(IdealPower - DzMarch809.iloc[i][fieldActivePower]) / 6 EPLostBadTotal += EPLost nBadTotal += 1 EPOverTotal = 0 nOverTotal = 0 for i in range(len(DzMarch809)): if DzMarch809Sel[i] == 3: EPOver = (DzMarch809.iloc[i][fieldActivePower] - PRated) / 6 EPOverTotal += EPOver nOverTotal += 1 EPLostPerformTotal = 0 for i in range(len(DzMarch809)): nWhichBinI = 0 for m in range(BB - 1): if IdealCurve.iloc[m][fieldWindSpeed] < DzMarch809.iloc[i][fieldWindSpeed] <= IdealCurve.iloc[m + 1][fieldWindSpeed]: nWhichBinI = m break if nWhichBinI > 0: IdealPower = (DzMarch809.iloc[i][fieldWindSpeed] - IdealCurve.iloc[nWhichBinI][fieldWindSpeed]) / (IdealCurve.iloc[nWhichBinI + 1][fieldWindSpeed] - IdealCurve.iloc[nWhichBinI] [fieldWindSpeed]) * (IdealCurve.iloc[nWhichBinI + 1][fieldActivePower] - IdealCurve.iloc[nWhichBinI][fieldActivePower]) + IdealCurve.iloc[nWhichBinI][fieldActivePower] EPLostPerformTotal += (IdealPower - DzMarch809.iloc[i][fieldActivePower]) / 6 EPIdealTotal = EPActualTotal + EPLostStopTotal + EPLostBadTotal + \ EPLostPerformTotal if EPLostPerformTotal >= 0 else EPActualTotal + \ EPLostStopTotal + EPLostBadTotal RemoveOverEP = 0 for i in range(len(DzMarch809)): if DzMarch809Sel[i] == 2: nWhichBin = 0 for m in range(BB - 1): if IdealCurve.iloc[m][fieldWindSpeed] < DzMarch809.iloc[i][fieldWindSpeed] <= IdealCurve.iloc[m + 1][fieldWindSpeed]: nWhichBin = m break if nWhichBin > 0: IdealPower = (DzMarch809.iloc[i][fieldWindSpeed] - IdealCurve.iloc[nWhichBin][fieldWindSpeed]) / (IdealCurve.iloc[nWhichBin + 1][fieldWindSpeed] - IdealCurve.iloc[nWhichBin] [fieldWindSpeed]) * (IdealCurve.iloc[nWhichBin + 1][fieldActivePower] - IdealCurve.iloc[nWhichBin][fieldActivePower]) + IdealCurve.iloc[nWhichBin][fieldActivePower] RemoveOverEP += (DzMarch809.iloc[i] [fieldActivePower] - IdealPower) / 6 for i in range(len(DzMarch809)): if DzMarch809.iloc[i][fieldActivePower] > PRated: RemoveOverEP += (DzMarch809.iloc[i][fieldActivePower] - PRated) / 6 return EPLostStopTotal, EPLostBadTotal, EPOverTotal, EPLostPerformTotal, EPIdealTotal - RemoveOverEP # 计算实测功率曲线函数 def calculate_measured_power_curve(PVDot, VRated, PRated): XBinNumber = np.ones(50) PCurve = np.zeros((50, 2)) PCurve[:, 0] = np.arange(0.5, 25.5, 0.5) XBinSum = np.zeros((50, 2)) for i in range(len(PVDot)): nWhichBin = 0 for b in range(50): if (b * 0.5 - 0.25) < PVDot.iloc[i][fieldWindSpeed] <= (b * 0.5 + 0.25): nWhichBin = b break if nWhichBin > 0: XBinSum[nWhichBin, 0] += PVDot.iloc[i][fieldWindSpeed] XBinSum[nWhichBin, 1] += PVDot.iloc[i][fieldActivePower] XBinNumber[nWhichBin] += 1 XBinNumber -= 1 for b in range(50): if XBinNumber[b] > 0: PCurve[b, 0] = XBinSum[b, 0] / XBinNumber[b] PCurve[b, 1] = XBinSum[b, 1] / XBinNumber[b] VRatedNum = int(VRated / 0.5) for m in range(VRatedNum, 50): if PCurve[m, 1] == 0: PCurve[m, 1] = PRated return PCurve # 计算标准正则功率曲线函数 def calculate_normalized_power_curve(IdealCurve, VRated, PRated): PCurveNorm = np.zeros((50, 2)) VRatedNum = int(VRated / 0.5) # 15m/s以上为额定功率 high_wind_speeds = np.arange(15, 25.5, 0.5) PCurveNorm[VRatedNum:VRatedNum+len(high_wind_speeds), 0] = high_wind_speeds PCurveNorm[VRatedNum:VRatedNum+len(high_wind_speeds), 1] = PRated # 15m/s以下正则功率曲线 VSpeed = np.arange(0.5, 15.5, 0.5) CurveData = IdealCurve[IdealCurve[fieldWindSpeed] <= 15].to_numpy() # 提取风速<=15的数据 for i, v in enumerate(VSpeed): if i < len(CurveData) - 1: # 插值计算 x0, y0 = CurveData[i] x1, y1 = CurveData[i + 1] PCurveNorm[i, 0] = v PCurveNorm[i, 1] = y0 + (v - x0) * (y1 - y0) / (x1 - x0) else: PCurveNorm[i, 0] = v PCurveNorm[i, 1] = PRated # 防止超出范围的情况 return PCurveNorm # 保存结果函数 def save_results(PCurve, PCurveNorm, EPKW, EPPer, outputDir='./output/A01'): if not os.path.exists(outputDir): os.makedirs(outputDir) pd.DataFrame(PCurve, columns=[fieldWindSpeed, fieldActivePower]).to_csv( os.path.join(outputDir, 'PCurve.csv'), index=False) pd.DataFrame(PCurveNorm, columns=[fieldWindSpeed, fieldActivePower]).to_csv( os.path.join(outputDir, 'PCurveNorm.csv'), index=False) pd.DataFrame([EPKW], columns=['EPIdealTotal', 'EPActualTotal', 'EPLostStopTotal', 'EPLostBadLimitTotal', 'EPLostPerformTotal', 'EPLostBadTotal', 'EPLostLimitTotal', 'EPOverTotal', 'WindSpeedAvr', 'TurbineRunRate']).to_csv(os.path.join(outputDir, 'EPKW.csv'), index=False) pd.DataFrame([EPPer], columns=['Percent1', 'Percent2', 'Percent3', 'Percent4', 'Percent5', 'Percent6', 'Percent7', 'Percent8', 'WindSpeedAvr', 'TurbineRunRate']).to_csv(os.path.join(outputDir, 'EPPer.csv'), index=False) # 绘制结果函数,使用 Plotly def plot_results(PVBad, PVLimit, PVDot, PCurve, IdealCurve): fig = go.Figure() # 添加坏点数据 if not PVBad.empty: fig.add_trace(go.Scatter(x=PVBad[fieldWindSpeed], y=PVBad[fieldActivePower], mode='markers', name='坏点', marker=dict(color='red'))) # 添加限电点数据 if not PVLimit.empty: fig.add_trace(go.Scatter(x=PVLimit[fieldWindSpeed], y=PVLimit[fieldActivePower], mode='markers', name='限功率', marker=dict(color='blue'))) # 添加正常点数据 if not PVDot.empty: fig.add_trace(go.Scatter(x=PVDot[fieldWindSpeed], y=PVDot[fieldActivePower], mode='markers', name='好点', marker=dict(color='black'))) # 添加实测功率曲线 if PCurve.shape[0] > 0: fig.add_trace(go.Scatter( x=PCurve[:, 0], y=PCurve[:, 1], mode='lines+markers', name='实测功率曲线', line=dict(color='green'))) # 添加设计功率曲线 if IdealCurve.shape[0] > 0: fig.add_trace(go.Scatter(x=IdealCurve[:, 0], y=IdealCurve[:, 1], mode='lines+markers', name='合同功率曲线', line=dict(color='yellow'))) # 更新布局 fig.update_layout( title={'text':'风力机功率散点数据分类标识','x':0.5}, xaxis_title=fieldWindSpeed, yaxis_title=fieldActivePower, legend=dict(x=0.01, y=0.99), template='plotly_white' ) # fig.show() outputDir = './output/A01' filePath = os.path.join(outputDir, f'power_scatter.html') fig.write_html(filePath) # 主函数 def main(): # 读取数据 March809, IdealCurve = read_data() # 计算统计信息 AA, BB, PNum, EPActualTotal, WindSpeedAvr, TurbineRunRate, EPIdealTotalAAA = calculate_statistics( March809, IdealCurve) # 分类数据 DzMarch809, XBoxNumber = classify_data(March809, PNum, VNum) # 计算百分比 PBoxPercent, VBoxPercent = compute_percentages(XBoxNumber, PNum, VNum) # 查找主带 DotDense, DotDenseLeftRight, PowerLimit, WidthAverage, WidthVar, PowerBandWidth, PBoxMaxIndex = find_main_band( PBoxPercent, PNum, VNum, XBoxNumber) # 标记坏点 DzMarch809Sel = mark_bad_points( DzMarch809, DotDenseLeftRight, PBoxMaxIndex, PowerLimit, PNum, VNum, XBoxNumber, PBoxPercent) # 标识限负荷数据点 PVLimitArray, DzMarch809Sel = identify_limit_load_data( DzMarch809, DzMarch809Sel, PRated) # 初始化存储好点和坏点的 DataFrame PVBad = pd.DataFrame(columns=DzMarch809.columns) PVLimit = pd.DataFrame(columns=DzMarch809.columns) PVDot = pd.DataFrame(columns=DzMarch809.columns) # 存储好点和坏点 for i in range(len(DzMarch809)): if DzMarch809Sel[i] in [1, 2, 3]: if not DzMarch809.iloc[[i]].isna().all().all(): PVBad = pd.concat( [PVBad, DzMarch809.iloc[[i]]], ignore_index=True) elif DzMarch809Sel[i] == 4: if not DzMarch809.iloc[[i]].isna().all().all(): PVLimit = pd.concat( [PVLimit, DzMarch809.iloc[[i]]], ignore_index=True) else: if not DzMarch809.iloc[[i]].isna().all().all(): PVDot = pd.concat( [PVDot, DzMarch809.iloc[[i]]], ignore_index=True) # 计算能量损失 EPLostStopTotal, EPLostBadTotal, EPOverTotal, EPLostPerformTotal, EPIdealTotal = calculate_energy_loss( DzMarch809Sel, DzMarch809, IdealCurve, PNum, BB, PRated, EPIdealTotalAAA, EPActualTotal) EPKW = [EPIdealTotal, EPActualTotal, EPLostStopTotal, EPLostBadTotal, EPLostPerformTotal, EPLostBadTotal, 0, EPOverTotal, WindSpeedAvr, TurbineRunRate] EPPer = [100, EPActualTotal / EPIdealTotal * 100, EPLostStopTotal / EPIdealTotal * 100, EPLostBadTotal / EPIdealTotal * 100, EPLostPerformTotal / EPIdealTotal * 100, EPLostBadTotal / EPIdealTotal * 100, 0, EPOverTotal / EPActualTotal * 100, WindSpeedAvr, TurbineRunRate] # 计算实测功率曲线 PCurve = calculate_measured_power_curve(PVDot, VRated, PRated) # 计算标准正则功率曲线 PCurveNorm = calculate_normalized_power_curve(IdealCurve, VRated, PRated) # 保存结果 save_results(PCurve, PCurveNorm, EPKW, EPPer) # 绘制结果 plot_results(PVBad, PVLimit, PVDot, PCurve, IdealCurve.to_numpy()) if __name__ == "__main__": main()