|
|
@@ -0,0 +1,780 @@
|
|
|
+package com.ufo.project.data.service.impl;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.YearMonth;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Map.Entry;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import com.ufo.base.common.utils.StringUtils;
|
|
|
+import com.ufo.module.system.domain.SysDictData;
|
|
|
+import com.ufo.module.system.service.ISysDictTypeService;
|
|
|
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportQuery;
|
|
|
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
|
|
|
+import com.ufo.project.data.service.IUltraShortTermPowerForecastReportService;
|
|
|
+import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastIssued;
|
|
|
+import com.ufo.project.forecast.supershort.domain.DataUltraShortTermPowerForecasting;
|
|
|
+import com.ufo.project.forecast.supershort.service.IDataSuperShortTermForecastIssuedService;
|
|
|
+import com.ufo.project.forecast.supershort.service.IDataUltraShortTermPowerForecastingService;
|
|
|
+import com.ufo.project.model.domain.DataElectricityLocal;
|
|
|
+import com.ufo.project.model.service.IDataElectricityLocalService;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 超短期预测报表Service业务层处理
|
|
|
+ *
|
|
|
+ * @author ufo
|
|
|
+ * @date 2026-09-07
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShortTermPowerForecastReportService
|
|
|
+{
|
|
|
+ private static final int SCALE = 4;
|
|
|
+ private static final long SECONDS_PER_DAY = 86400L;
|
|
|
+ private static final long SECONDS_PER_HOUR = 3600L;
|
|
|
+ private static final long SECONDS_PER_4H = SECONDS_PER_HOUR * 4L;
|
|
|
+
|
|
|
+ private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
|
+ private static final DateTimeFormatter YEAR_FMT = DateTimeFormatter.ofPattern("yyyy");
|
|
|
+
|
|
|
+ private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
|
|
|
+ private static final ZoneOffset ZONE_8 = ZoneOffset.ofHours(8);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IDataElectricityLocalService electricityLocalService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IDataSuperShortTermForecastIssuedService superShortService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IDataUltraShortTermPowerForecastingService ultraForecastService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 超短期预测报表查询
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<UltraShortTermPowerForecastReportVO> selectReportList(UltraShortTermPowerForecastReportQuery query)
|
|
|
+ {
|
|
|
+ String type = query.getType();
|
|
|
+ String entityId = query.getEntityId();
|
|
|
+ String startTime = query.getStartTime();
|
|
|
+ String endTime = query.getEndTime();
|
|
|
+
|
|
|
+ // 1. 获取实发功率数据
|
|
|
+ String startStr = startTime + " 00:00:00";
|
|
|
+ String endStr = endTime + " 23:59:59";
|
|
|
+ List<DataElectricityLocal> actualList = electricityLocalService.selectActualPowerList(
|
|
|
+ parseLong(entityId), startStr, endStr);
|
|
|
+ if (actualList == null || actualList.isEmpty())
|
|
|
+ {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取超短期预测功率数据
|
|
|
+ Long startSec = toUnixSeconds(startStr);
|
|
|
+ Long endSec = toUnixSeconds(endStr);
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastList = selectSuperShortByTimeRange(entityId, startSec, endSec);
|
|
|
+
|
|
|
+ // 3. 获取超短期预报数据(提前4h)
|
|
|
+ Long startSec4h = startSec - SECONDS_PER_4H;
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraForecastList = ultraForecastService
|
|
|
+ .selectByTimeRange(entityId, startSec4h, endSec);
|
|
|
+
|
|
|
+ // 4. 按基准类型分组
|
|
|
+ switch (type)
|
|
|
+ {
|
|
|
+ case "daily":
|
|
|
+ return buildDailyReport(actualList, forecastList, ultraForecastList);
|
|
|
+ case "monthly":
|
|
|
+ return buildMonthlyReport(actualList, forecastList, ultraForecastList);
|
|
|
+ case "yearly":
|
|
|
+ return buildYearlyReport(actualList, forecastList, ultraForecastList);
|
|
|
+ default:
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建日报
|
|
|
+ */
|
|
|
+ private List<UltraShortTermPowerForecastReportVO> buildDailyReport(
|
|
|
+ List<DataElectricityLocal> actualList,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastList,
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraForecastList)
|
|
|
+ {
|
|
|
+ Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
|
|
|
+ Map<String, List<DataSuperShortTermForecastIssued>> forecastDailyMap = groupForecastByDay(forecastList);
|
|
|
+ Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
|
|
|
+
|
|
|
+ List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
|
|
|
+ for (Entry<String, List<DataElectricityLocal>> entry : dailyMap.entrySet())
|
|
|
+ {
|
|
|
+ String day = entry.getKey();
|
|
|
+ List<DataElectricityLocal> records = entry.getValue();
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForDay(day, ultraForecastMap);
|
|
|
+
|
|
|
+ UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
|
|
|
+ vo.setTime(day);
|
|
|
+ computeMetrics(vo, records, forecastRecords, ultraRecords);
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建月报
|
|
|
+ */
|
|
|
+ private List<UltraShortTermPowerForecastReportVO> buildMonthlyReport(
|
|
|
+ List<DataElectricityLocal> actualList,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastList,
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraForecastList)
|
|
|
+ {
|
|
|
+ Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
|
|
|
+ Map<String, List<DataSuperShortTermForecastIssued>> forecastMonthlyMap = groupForecastByMonth(forecastList);
|
|
|
+ Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
|
|
|
+
|
|
|
+ List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
|
|
|
+ for (Entry<String, List<DataElectricityLocal>> entry : monthlyMap.entrySet())
|
|
|
+ {
|
|
|
+ String month = entry.getKey();
|
|
|
+ List<DataElectricityLocal> records = entry.getValue();
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords =
|
|
|
+ forecastMonthlyMap.getOrDefault(month, Collections.emptyList());
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForMonth(month, ultraForecastMap);
|
|
|
+
|
|
|
+ UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
|
|
|
+ vo.setTime(month);
|
|
|
+ computeMetrics(vo, records, forecastRecords, ultraRecords);
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建年报
|
|
|
+ */
|
|
|
+ private List<UltraShortTermPowerForecastReportVO> buildYearlyReport(
|
|
|
+ List<DataElectricityLocal> actualList,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastList,
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraForecastList)
|
|
|
+ {
|
|
|
+ Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
|
|
|
+ Map<String, List<DataSuperShortTermForecastIssued>> forecastYearlyMap = groupForecastByYear(forecastList);
|
|
|
+ Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
|
|
|
+
|
|
|
+ List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
|
|
|
+ for (Entry<String, List<DataElectricityLocal>> entry : yearlyMap.entrySet())
|
|
|
+ {
|
|
|
+ String year = entry.getKey();
|
|
|
+ List<DataElectricityLocal> records = entry.getValue();
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords =
|
|
|
+ forecastYearlyMap.getOrDefault(year, Collections.emptyList());
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForYear(year, ultraForecastMap);
|
|
|
+
|
|
|
+ UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
|
|
|
+ vo.setTime(year);
|
|
|
+ computeMetrics(vo, records, forecastRecords, ultraRecords);
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算指标
|
|
|
+ */
|
|
|
+ private void computeMetrics(UltraShortTermPowerForecastReportVO vo,
|
|
|
+ List<DataElectricityLocal> actualRecords,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords,
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraRecords)
|
|
|
+ {
|
|
|
+ // 1. 开机容量
|
|
|
+ BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
|
|
|
+ vo.setOperatingCapacity(operatingCapacity);
|
|
|
+
|
|
|
+ // 2. 实发功率
|
|
|
+ BigDecimal actualPower = computeActualPower(actualRecords);
|
|
|
+ vo.setActualPower(actualPower);
|
|
|
+
|
|
|
+ // 3. 预测功率(超短期预测功率)
|
|
|
+ BigDecimal forecastPower = computeForecastPower(forecastRecords);
|
|
|
+ vo.setForecastPower(forecastPower);
|
|
|
+
|
|
|
+ // 4. RMSE
|
|
|
+ BigDecimal rmse = computeRmse(actualRecords, forecastRecords);
|
|
|
+ vo.setRootMeanSquareError(rmse);
|
|
|
+
|
|
|
+ // 5. MAE
|
|
|
+ BigDecimal mae = computeMae(actualRecords, forecastRecords);
|
|
|
+ vo.setMeanAbsoluteError(mae);
|
|
|
+
|
|
|
+ // 6. 相关性系数
|
|
|
+ BigDecimal corr = computeCorrelation(actualRecords, forecastRecords);
|
|
|
+ vo.setCorrelationCoefficientR(corr);
|
|
|
+
|
|
|
+ // 7. 4小时前预测准确率
|
|
|
+ BigDecimal accuracy = computeAheadForecastAccuracy4h(actualRecords, ultraRecords, operatingCapacity);
|
|
|
+ vo.setAheadForecastAccuracy4h(accuracy);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 基础计算 ====================
|
|
|
+
|
|
|
+ private BigDecimal computeOperatingCapacity(List<DataElectricityLocal> records)
|
|
|
+ {
|
|
|
+ double maxPower = 0.0;
|
|
|
+ for (DataElectricityLocal r : records)
|
|
|
+ {
|
|
|
+ if (r.getDeviceStatus() != null && r.getDeviceStatus() == 0
|
|
|
+ && r.getGridStatus() != null && r.getGridStatus() == 1)
|
|
|
+ {
|
|
|
+ Double ap = r.getActualPower();
|
|
|
+ if (ap != null && ap > maxPower)
|
|
|
+ {
|
|
|
+ maxPower = ap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return BigDecimal.valueOf(maxPower / 1000.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal computeActualPower(List<DataElectricityLocal> records)
|
|
|
+ {
|
|
|
+ double sum = 0.0;
|
|
|
+ int count = 0;
|
|
|
+ for (DataElectricityLocal r : records)
|
|
|
+ {
|
|
|
+ if (r.getGridStatus() != null && r.getGridStatus() == 1)
|
|
|
+ {
|
|
|
+ Double ap = r.getActualPower();
|
|
|
+ if (ap != null)
|
|
|
+ {
|
|
|
+ sum += ap;
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (count == 0) return BigDecimal.ZERO;
|
|
|
+ return BigDecimal.valueOf(sum / count / 1000.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal computeForecastPower(List<DataSuperShortTermForecastIssued> records)
|
|
|
+ {
|
|
|
+ double sum = 0.0;
|
|
|
+ int count = 0;
|
|
|
+ for (DataSuperShortTermForecastIssued r : records)
|
|
|
+ {
|
|
|
+ String power = r.getPower();
|
|
|
+ if (StringUtils.isNotEmpty(power))
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ sum += Double.parseDouble(power.trim());
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ catch (NumberFormatException e)
|
|
|
+ {
|
|
|
+ // skip
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (count == 0) return BigDecimal.ZERO;
|
|
|
+ return BigDecimal.valueOf(sum / count / 1000.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords)
|
|
|
+ {
|
|
|
+ Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
|
|
|
+ if (forecastMap.isEmpty()) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double sumSquaredError = 0.0;
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ for (DataElectricityLocal r : actualRecords)
|
|
|
+ {
|
|
|
+ if (r.getTm() == null) continue;
|
|
|
+ long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
|
|
|
+ Double fp = forecastMap.get(ts);
|
|
|
+ if (fp == null) continue;
|
|
|
+
|
|
|
+ Double ap = r.getActualPower();
|
|
|
+ if (ap == null) continue;
|
|
|
+
|
|
|
+ double diffMW = (ap - fp) / 1000.0;
|
|
|
+ sumSquaredError += diffMW * diffMW;
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count == 0) return BigDecimal.ZERO;
|
|
|
+ return BigDecimal.valueOf(Math.sqrt(sumSquaredError / count)).setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords)
|
|
|
+ {
|
|
|
+ Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
|
|
|
+ if (forecastMap.isEmpty()) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double sumAbsError = 0.0;
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ for (DataElectricityLocal r : actualRecords)
|
|
|
+ {
|
|
|
+ if (r.getTm() == null) continue;
|
|
|
+ long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
|
|
|
+ Double fp = forecastMap.get(ts);
|
|
|
+ if (fp == null) continue;
|
|
|
+
|
|
|
+ Double ap = r.getActualPower();
|
|
|
+ if (ap == null) continue;
|
|
|
+
|
|
|
+ double absErrorMW = Math.abs(ap - fp) / 1000.0;
|
|
|
+ sumAbsError += absErrorMW;
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count == 0) return BigDecimal.ZERO;
|
|
|
+ return BigDecimal.valueOf(sumAbsError / count).setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
|
|
|
+ List<DataSuperShortTermForecastIssued> forecastRecords)
|
|
|
+ {
|
|
|
+ Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
|
|
|
+ if (forecastMap.isEmpty()) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ List<Double> xList = new ArrayList<>();
|
|
|
+ List<Double> yList = new ArrayList<>();
|
|
|
+
|
|
|
+ for (DataElectricityLocal r : actualRecords)
|
|
|
+ {
|
|
|
+ if (r.getTm() == null) continue;
|
|
|
+ long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
|
|
|
+ Double fp = forecastMap.get(ts);
|
|
|
+ if (fp == null) continue;
|
|
|
+
|
|
|
+ Double ap = r.getActualPower();
|
|
|
+ if (ap == null) continue;
|
|
|
+
|
|
|
+ xList.add(ap / 1000.0);
|
|
|
+ yList.add(fp / 1000.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ int n = xList.size();
|
|
|
+ if (n < 2) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double meanX = xList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
|
|
|
+ double meanY = yList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
|
|
|
+
|
|
|
+ double numerator = 0.0;
|
|
|
+ double sumSqX = 0.0;
|
|
|
+ double sumSqY = 0.0;
|
|
|
+
|
|
|
+ for (int i = 0; i < n; i++)
|
|
|
+ {
|
|
|
+ double dx = xList.get(i) - meanX;
|
|
|
+ double dy = yList.get(i) - meanY;
|
|
|
+ numerator += dx * dy;
|
|
|
+ sumSqX += dx * dx;
|
|
|
+ sumSqY += dy * dy;
|
|
|
+ }
|
|
|
+
|
|
|
+ double denominator = Math.sqrt(sumSqX * sumSqY);
|
|
|
+ if (denominator == 0.0) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double r = numerator / denominator;
|
|
|
+ return BigDecimal.valueOf(r).setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算4小时前预测准确率
|
|
|
+ *
|
|
|
+ * 对每个实际数据点,找到start_time匹配的超短期预报记录
|
|
|
+ * 从该记录的power15~power240字段提取16个预测值,取算术平均得P_Pi
|
|
|
+ * error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
|
|
|
+ * ACC = 1 - sqrt((1/N) * sum(error_i^2))
|
|
|
+ */
|
|
|
+ private BigDecimal computeAheadForecastAccuracy4h(List<DataElectricityLocal> actualRecords,
|
|
|
+ List<DataUltraShortTermPowerForecasting> ultraRecords,
|
|
|
+ BigDecimal operatingCapacity)
|
|
|
+ {
|
|
|
+ if (ultraRecords == null || ultraRecords.isEmpty()) return BigDecimal.ZERO;
|
|
|
+ if (operatingCapacity == null || operatingCapacity.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double capacityMW = operatingCapacity.doubleValue();
|
|
|
+ double thresholdMW = Math.max(0.2 * capacityMW, 0.001); // 防止分母过小
|
|
|
+
|
|
|
+ // 构建超短期预报数据映射: start_time(秒) -> 记录
|
|
|
+ Map<Long, DataUltraShortTermPowerForecasting> ultraMap = new LinkedHashMap<>();
|
|
|
+ for (DataUltraShortTermPowerForecasting r : ultraRecords)
|
|
|
+ {
|
|
|
+ if (r.getStartTime() != null)
|
|
|
+ {
|
|
|
+ ultraMap.put(r.getStartTime(), r);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ double sumSquaredError = 0.0;
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ for (DataElectricityLocal actual : actualRecords)
|
|
|
+ {
|
|
|
+ if (actual.getTm() == null || actual.getActualPower() == null) continue;
|
|
|
+
|
|
|
+ // 实际数据的unix秒时间戳
|
|
|
+ long actualTs = actual.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
|
|
|
+
|
|
|
+ // 查找start_time匹配的超短期预报记录
|
|
|
+ DataUltraShortTermPowerForecasting ultra = ultraMap.get(actualTs);
|
|
|
+ if (ultra == null) continue;
|
|
|
+
|
|
|
+ // 计算P_Pi = (power15 + power30 + ... + power240) / 16 的算术平均
|
|
|
+ double ppSum = 0.0;
|
|
|
+ int ppCount = 0;
|
|
|
+
|
|
|
+ String[] powerFields = {
|
|
|
+ "power15", "power30", "power45", "power60",
|
|
|
+ "power75", "power90", "power105", "power120",
|
|
|
+ "power135", "power150", "power165", "power180",
|
|
|
+ "power195", "power210", "power225", "power240"
|
|
|
+ };
|
|
|
+
|
|
|
+ for (String field : powerFields)
|
|
|
+ {
|
|
|
+ Double value = getPowerField(ultra, field);
|
|
|
+ if (value != null)
|
|
|
+ {
|
|
|
+ ppSum += value;
|
|
|
+ ppCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ppCount == 0) continue;
|
|
|
+ double ppMean = ppSum / ppCount; // P_Pi (kW)
|
|
|
+
|
|
|
+ // P_Mi (kW)
|
|
|
+ double pm = actual.getActualPower();
|
|
|
+
|
|
|
+ // error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
|
|
|
+ double denominator = Math.max(Math.abs(pm), thresholdMW);
|
|
|
+ double error = (pm - ppMean) / denominator;
|
|
|
+
|
|
|
+ sumSquaredError += error * error;
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count == 0) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ double acc = 1.0 - Math.sqrt(sumSquaredError / count);
|
|
|
+ return BigDecimal.valueOf(acc).setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全获取Power字段值
|
|
|
+ */
|
|
|
+ private Double getPowerField(DataUltraShortTermPowerForecasting record, String fieldName)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ switch (fieldName)
|
|
|
+ {
|
|
|
+ case "power15": return parseDouble(record.getPower15());
|
|
|
+ case "power30": return parseDouble(record.getPower30());
|
|
|
+ case "power45": return parseDouble(record.getPower45());
|
|
|
+ case "power60": return parseDouble(record.getPower60());
|
|
|
+ case "power75": return parseDouble(record.getPower75());
|
|
|
+ case "power90": return parseDouble(record.getPower90());
|
|
|
+ case "power105": return parseDouble(record.getPower105());
|
|
|
+ case "power120": return parseDouble(record.getPower120());
|
|
|
+ case "power135": return parseDouble(record.getPower135());
|
|
|
+ case "power150": return parseDouble(record.getPower150());
|
|
|
+ case "power165": return parseDouble(record.getPower165());
|
|
|
+ case "power180": return parseDouble(record.getPower180());
|
|
|
+ case "power195": return parseDouble(record.getPower195());
|
|
|
+ case "power210": return parseDouble(record.getPower210());
|
|
|
+ case "power225": return parseDouble(record.getPower225());
|
|
|
+ case "power240": return parseDouble(record.getPower240());
|
|
|
+ default: return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建超短期预测数据映射: unix秒 -> power(kW)
|
|
|
+ */
|
|
|
+ private Map<Long, Double> buildForecastMap(List<DataSuperShortTermForecastIssued> records)
|
|
|
+ {
|
|
|
+ Map<Long, Double> map = new LinkedHashMap<>();
|
|
|
+ if (records == null) return map;
|
|
|
+
|
|
|
+ for (DataSuperShortTermForecastIssued r : records)
|
|
|
+ {
|
|
|
+ String power = r.getPower();
|
|
|
+ if (StringUtils.isEmpty(power)) continue;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ Double p = Double.parseDouble(power.trim());
|
|
|
+ if (r.getForecastTime() != null)
|
|
|
+ {
|
|
|
+ map.put(r.getForecastTime(), p);
|
|
|
+ }
|
|
|
+ if (r.getStartTime() != null)
|
|
|
+ {
|
|
|
+ map.putIfAbsent(r.getStartTime(), p);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (NumberFormatException e)
|
|
|
+ {
|
|
|
+ // skip
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全解析String为Double
|
|
|
+ */
|
|
|
+ private Double parseDouble(String value)
|
|
|
+ {
|
|
|
+ if (value == null || value.isEmpty()) return 0.0;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return Double.parseDouble(value.trim());
|
|
|
+ }
|
|
|
+ catch (NumberFormatException e)
|
|
|
+ {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全解析String为Long
|
|
|
+ */
|
|
|
+ private Long parseLong(String value)
|
|
|
+ {
|
|
|
+ if (value == null || value.isEmpty()) return 0L;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return Long.parseLong(value.trim());
|
|
|
+ }
|
|
|
+ catch (NumberFormatException e)
|
|
|
+ {
|
|
|
+ return 0L;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将yyyy-MM-dd HH:mm:ss字符串转换为unix秒级时间戳
|
|
|
+ */
|
|
|
+ private Long toUnixSeconds(String dateTime)
|
|
|
+ {
|
|
|
+ return java.time.LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
|
|
+ .atZone(ZONE_8)
|
|
|
+ .toInstant()
|
|
|
+ .getEpochSecond();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按日分组
|
|
|
+ */
|
|
|
+ private Map<String, List<DataElectricityLocal>> groupByDay(List<DataElectricityLocal> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getTm() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r -> LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI).format(DATE_FMT),
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按月分组
|
|
|
+ */
|
|
|
+ private Map<String, List<DataElectricityLocal>> groupByMonth(List<DataElectricityLocal> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getTm() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r ->
|
|
|
+ {
|
|
|
+ LocalDate ld = LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI);
|
|
|
+ return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
|
|
|
+ },
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按年分组
|
|
|
+ */
|
|
|
+ private Map<String, List<DataElectricityLocal>> groupByYear(List<DataElectricityLocal> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getTm() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r -> String.valueOf(LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI).getYear()),
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按日分组超短期预测数据
|
|
|
+ */
|
|
|
+ private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByDay(
|
|
|
+ List<DataSuperShortTermForecastIssued> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getForecastTime() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r -> LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY).format(DATE_FMT),
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按月分组超短期预测数据
|
|
|
+ */
|
|
|
+ private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByMonth(
|
|
|
+ List<DataSuperShortTermForecastIssued> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getForecastTime() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r ->
|
|
|
+ {
|
|
|
+ LocalDate ld = LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY);
|
|
|
+ return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
|
|
|
+ },
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按年分组超短期预测数据
|
|
|
+ */
|
|
|
+ private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByYear(
|
|
|
+ List<DataSuperShortTermForecastIssued> records)
|
|
|
+ {
|
|
|
+ return records.stream()
|
|
|
+ .filter(r -> r.getForecastTime() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ r -> String.valueOf(LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY).getYear()),
|
|
|
+ LinkedHashMap::new,
|
|
|
+ Collectors.toList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按start_time秒分组超短期预报数据
|
|
|
+ */
|
|
|
+ private Map<Long, List<DataUltraShortTermPowerForecasting>> groupUltraForecastByStartSec(
|
|
|
+ List<DataUltraShortTermPowerForecasting> records)
|
|
|
+ {
|
|
|
+ Map<Long, List<DataUltraShortTermPowerForecasting>> map = new LinkedHashMap<>();
|
|
|
+ if (records == null) return map;
|
|
|
+ for (DataUltraShortTermPowerForecasting r : records)
|
|
|
+ {
|
|
|
+ if (r.getStartTime() != null)
|
|
|
+ {
|
|
|
+ map.computeIfAbsent(r.getStartTime(), k -> new ArrayList<>()).add(r);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集某日对应的超短期预报记录
|
|
|
+ */
|
|
|
+ private List<DataUltraShortTermPowerForecasting> collectUltraForDay(
|
|
|
+ String day, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
|
|
|
+ {
|
|
|
+ LocalDate ld = LocalDate.parse(day, DATE_FMT);
|
|
|
+ long dayStartSec = ld.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
|
|
|
+ return collectByStartSec(dayStartSec, SECONDS_PER_DAY, ultraMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集某月对应的超短期预报记录
|
|
|
+ */
|
|
|
+ private List<DataUltraShortTermPowerForecasting> collectUltraForMonth(
|
|
|
+ String month, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
|
|
|
+ {
|
|
|
+ YearMonth ym = YearMonth.parse(month, MONTH_FMT);
|
|
|
+ LocalDate startDate = ym.atDay(1);
|
|
|
+ LocalDate endDate = ym.atEndOfMonth();
|
|
|
+ long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
|
|
|
+ long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
|
|
|
+ long duration = endSec - startSec;
|
|
|
+ return collectByStartSecRange(startSec, duration, ultraMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集某年对应的超短期预报记录
|
|
|
+ */
|
|
|
+ private List<DataUltraShortTermPowerForecasting> collectUltraForYear(
|
|
|
+ String year, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
|
|
|
+ {
|
|
|
+ int y = Integer.parseInt(year);
|
|
|
+ LocalDate startDate = LocalDate.of(y, 1, 1);
|
|
|
+ LocalDate endDate = LocalDate.of(y, 12, 31);
|
|
|
+ long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
|
|
|
+ long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
|
|
|
+ long duration = endSec - startSec;
|
|
|
+ return collectByStartSecRange(startSec, duration, ultraMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据start_time范围收集超短期预报记录
|
|
|
+ */
|
|
|
+ private List<DataUltraShortTermPowerForecasting> collectByStartSec(
|
|
|
+ long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
|
|
|
+ {
|
|
|
+ long endSec = startSec + duration;
|
|
|
+ return ultraMap.keySet().stream()
|
|
|
+ .filter(k -> k >= startSec && k < endSec)
|
|
|
+ .flatMap(k -> ultraMap.get(k).stream())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<DataUltraShortTermPowerForecasting> collectByStartSecRange(
|
|
|
+ long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
|
|
|
+ {
|
|
|
+ long endSec = startSec + duration;
|
|
|
+ List<DataUltraShortTermPowerForecasting> result = new ArrayList<>();
|
|
|
+ for (long k = startSec; k < endSec; k += SECONDS_PER_DAY)
|
|
|
+ {
|
|
|
+ List<DataUltraShortTermPowerForecasting> dayRecords = collectByStartSec(k, SECONDS_PER_DAY, ultraMap);
|
|
|
+ result.addAll(dayRecords);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询超短期预测功率数据列表
|
|
|
+ */
|
|
|
+ private List<DataSuperShortTermForecastIssued> selectSuperShortByTimeRange(
|
|
|
+ String entityId, Long startTime, Long endTime)
|
|
|
+ {
|
|
|
+ DataSuperShortTermForecastIssued query = new DataSuperShortTermForecastIssued();
|
|
|
+ query.setEntityId(entityId);
|
|
|
+ query.setForecastStartTime(startTime);
|
|
|
+ query.setForecastEndTime(endTime);
|
|
|
+ return superShortService.selectDataSuperShortTermForecastIssuedList(query);
|
|
|
+ }
|
|
|
+}
|