| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- package com.usky.vpp.util;
- import com.usky.vpp.constant.VppTsdbConstants;
- import com.usky.vpp.service.vo.BaselinePointVO;
- import com.usky.vpp.service.vo.DeclaredCapacityPointVO;
- import java.math.BigDecimal;
- import java.math.RoundingMode;
- import java.time.LocalDate;
- import java.time.LocalDateTime;
- import java.time.LocalTime;
- import java.time.format.DateTimeFormatter;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashSet;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TreeMap;
- import java.util.function.BiFunction;
- /**
- * 基线管理计算辅助(典型日筛选、原始基线、修正系数、曲线构建)
- */
- public final class VppBaselineHelper {
- public static final int WEEKDAY_REFERENCE_COUNT = 5;
- public static final int NON_WEEKDAY_REFERENCE_COUNT = 3;
- public static final int CORRECTION_HOURS = 2;
- public static final int INTERVAL_MINUTES = 5;
- public static final BigDecimal K_MIN = new BigDecimal("0.7");
- public static final BigDecimal K_MAX = new BigDecimal("1.2");
- public static final BigDecimal K_DEFAULT = BigDecimal.ONE;
- /** 总有功功率指标(文档 p + 存量字段兼容) */
- public static final List<String> POWER_METRICS;
- private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
- private static final DateTimeFormatter TIME_WITH_SEC_FMT = DateTimeFormatter.ofPattern("HH:mm:ss");
- private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- static {
- List<String> metrics = new ArrayList<>();
- metrics.add("p");
- metrics.addAll(VppTsdbConstants.ACTIVE_POWER_METRICS);
- POWER_METRICS = Collections.unmodifiableList(metrics);
- }
- private VppBaselineHelper() {
- }
- /**
- * 响应日是否为工作日(Hutool 法定节假日 + 调休规则)
- */
- public static boolean isWorkdayResponse(LocalDate date) {
- return VppWorkdayHelper.isWorkday(date);
- }
- /**
- * @deprecated 使用 {@link #isWorkdayResponse(LocalDate)}
- */
- @Deprecated
- public static boolean isWeekday(LocalDate date) {
- return isWorkdayResponse(date);
- }
- /**
- * 选取典型历史日:工作日取前 5 个工作日,非工作日取前 3 个非工作日;
- * 仅剔除响应当日与历史响应日(不含节假日前后日期)。
- */
- public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
- boolean workday,
- Set<LocalDate> excludeDates,
- int requiredCount) {
- Set<LocalDate> excludes = new HashSet<>(excludeDates);
- excludes.add(responseDate);
- List<LocalDate> result = new ArrayList<>();
- LocalDate cursor = responseDate.minusDays(1);
- int guard = 0;
- while (result.size() < requiredCount && guard < 400) {
- if (excludes.contains(cursor)) {
- cursor = cursor.minusDays(1);
- guard++;
- continue;
- }
- if (VppWorkdayHelper.isWorkday(cursor) == workday) {
- result.add(cursor);
- }
- cursor = cursor.minusDays(1);
- guard++;
- }
- return result;
- }
- /**
- * 多历史日同时段各时间点负荷求平均(先按日汇总设备负荷,再跨日取均值)。
- * <p>通过 historyLoader 按日调用 TSDB {@code queryHistoryDeviceData} 获取原始时序后在本模块聚合。</p>
- */
- public static Map<String, BigDecimal> aggregateBaselineAvgByTimePoint(
- List<LocalDate> referenceDates,
- LocalTime periodStart,
- LocalTime periodEnd,
- int intervalMinutes,
- BigDecimal correctionFactor,
- BiFunction<LocalDateTime, LocalDateTime, Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>>> historyLoader) {
- if (referenceDates == null || referenceDates.isEmpty() || historyLoader == null) {
- return Collections.emptyMap();
- }
- LocalTime dayStart = periodStart != null ? periodStart : LocalTime.MIN;
- LocalTime dayEnd = periodEnd != null ? periodEnd : LocalTime.of(23, 59, 59);
- int interval = intervalMinutes > 0 ? intervalMinutes : INTERVAL_MINUTES;
- Map<String, List<BigDecimal>> samplesByTime = new TreeMap<>();
- for (LocalDate date : referenceDates) {
- LocalDateTime start = LocalDateTime.of(date, dayStart);
- LocalDateTime end = LocalDateTime.of(date, dayEnd);
- if (end.isBefore(start)) {
- continue;
- }
- Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> dayData = historyLoader.apply(start, end);
- Map<String, BigDecimal> dayBuckets = buildActualLoadByTimePoint(dayData, interval);
- if (dayBuckets.isEmpty()) {
- continue;
- }
- for (Map.Entry<String, BigDecimal> entry : dayBuckets.entrySet()) {
- samplesByTime.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(entry.getValue());
- }
- }
- Map<String, BigDecimal> result = new LinkedHashMap<>();
- for (Map.Entry<String, List<BigDecimal>> entry : samplesByTime.entrySet()) {
- BigDecimal avg = average(entry.getValue());
- if (correctionFactor != null) {
- avg = avg.multiply(correctionFactor);
- }
- result.put(entry.getKey(), scale(avg));
- }
- return result;
- }
- /**
- * 将设备历史时序聚合为 HH:mm 时间点总负荷。
- */
- public static Map<String, BigDecimal> buildActualLoadByTimePoint(
- Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
- int intervalMinutes) {
- if (deviceData == null || deviceData.isEmpty()) {
- return Collections.emptyMap();
- }
- Map<String, List<BigDecimal>> samples = new TreeMap<>();
- for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceData.values()) {
- TreeMap<LocalDateTime, BigDecimal> series = pickSeries(metricMap);
- if (series == null || series.isEmpty()) {
- continue;
- }
- Map<String, List<BigDecimal>> deviceBuckets = bucketSeries(series, intervalMinutes);
- for (Map.Entry<String, List<BigDecimal>> entry : deviceBuckets.entrySet()) {
- BigDecimal deviceAvg = average(entry.getValue());
- samples.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(deviceAvg);
- }
- }
- Map<String, BigDecimal> result = new LinkedHashMap<>();
- for (Map.Entry<String, List<BigDecimal>> entry : samples.entrySet()) {
- result.put(entry.getKey(), scale(sum(entry.getValue())));
- }
- return result;
- }
- /**
- * 计算日内修正系数 K = 响应开始前 2 小时今日实测 / 同时段原始基线,并限制在 [0.7, 1.2]。
- */
- public static BigDecimal calculateCorrectionFactor(LocalDateTime responseStartTime,
- Map<String, BigDecimal> todayActualByTime,
- Map<String, BigDecimal> originalBaselineByTime) {
- LocalDateTime windowStart = responseStartTime.minusHours(CORRECTION_HOURS);
- BigDecimal todayTotal = sumValuesInWindow(todayActualByTime, windowStart, responseStartTime);
- BigDecimal baselineTotal = sumValuesInWindow(originalBaselineByTime, windowStart, responseStartTime);
- if (baselineTotal == null || baselineTotal.compareTo(BigDecimal.ZERO) <= 0) {
- return K_DEFAULT;
- }
- if (todayTotal == null || todayTotal.compareTo(BigDecimal.ZERO) <= 0) {
- return K_DEFAULT;
- }
- BigDecimal k = todayTotal.divide(baselineTotal, 6, RoundingMode.HALF_UP);
- if (k.compareTo(K_MIN) < 0) {
- return K_MIN;
- }
- if (k.compareTo(K_MAX) > 0) {
- return K_MAX;
- }
- return k.setScale(4, RoundingMode.HALF_UP);
- }
- public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
- LocalDateTime responseStartTime,
- Map<String, BigDecimal> predictedBaselineByTime,
- Map<String, BigDecimal> todayActualByTime,
- Map<String, BigDecimal> yesterdayActualByTime) {
- return buildBaselinePoints(responseDate, responseStartTime, responseStartTime,
- predictedBaselineByTime, todayActualByTime, yesterdayActualByTime);
- }
- /**
- * @param todayActualUntil 当日实测曲线填充截止时间(含),用于响应后窗口展示
- */
- public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
- LocalDateTime responseStartTime,
- LocalDateTime todayActualUntil,
- Map<String, BigDecimal> predictedBaselineByTime,
- Map<String, BigDecimal> todayActualByTime,
- Map<String, BigDecimal> yesterdayActualByTime) {
- List<BaselinePointVO> points = new ArrayList<>();
- LocalDateTime actualUntil = todayActualUntil != null ? todayActualUntil : responseStartTime;
- for (Map.Entry<String, BigDecimal> entry : predictedBaselineByTime.entrySet()) {
- String timeKey = entry.getKey();
- LocalTime time = parseTimeKey(timeKey);
- LocalDateTime pointTime = LocalDateTime.of(responseDate, time);
- BaselinePointVO point = new BaselinePointVO();
- point.setTime(timeKey);
- point.setPredictedBaselineKw(scale(entry.getValue()));
- if (!pointTime.isAfter(actualUntil)) {
- point.setTodayActualKw(scale(todayActualByTime.get(timeKey)));
- }
- point.setYesterdayActualKw(scale(yesterdayActualByTime.get(timeKey)));
- points.add(point);
- }
- return points;
- }
- public static boolean hasTsdbPowerData(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceMetricData) {
- return VppCapabilityEvalHelper.hasTsdbPowerData(deviceMetricData, POWER_METRICS);
- }
- /*
- public static List<BaselinePointVO> buildMockBaselinePoints(Long siteId,
- LocalDate responseDate,
- LocalDateTime responseStartTime) {
- List<BaselinePointVO> points = new ArrayList<>();
- long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
- for (int minute = 0; minute < 1440; minute += INTERVAL_MINUTES) {
- LocalTime time = LocalTime.of(minute / 60, minute % 60);
- String timeKey = time.format(TIME_FMT);
- double hourFactor = mockHourFactor(time);
- double noise = 0.9 + pseudoRandom(seed, minute, 0) * 0.2;
- BigDecimal yesterday = scale(BigDecimal.valueOf(900 * hourFactor * noise));
- BigDecimal original = yesterday.multiply(BigDecimal.valueOf(0.98 + pseudoRandom(seed, minute, 1) * 0.04))
- .setScale(2, RoundingMode.HALF_UP);
- BigDecimal predicted = original.multiply(BigDecimal.valueOf(1.03)).setScale(2, RoundingMode.HALF_UP);
- BaselinePointVO point = new BaselinePointVO();
- point.setTime(timeKey);
- point.setPredictedBaselineKw(predicted);
- point.setYesterdayActualKw(yesterday);
- if (!responseDate.atTime(time).isAfter(responseStartTime)) {
- point.setTodayActualKw(scale(original.multiply(BigDecimal.valueOf(1.01 + pseudoRandom(seed, minute, 2) * 0.05))));
- }
- points.add(point);
- }
- return points;
- }
- public static BigDecimal mockCorrectionFactor(Long siteId, LocalDate responseDate) {
- long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
- double raw = 0.85 + pseudoRandom(seed, 99, 3) * 0.25;
- BigDecimal k = BigDecimal.valueOf(raw).setScale(4, RoundingMode.HALF_UP);
- if (k.compareTo(K_MIN) < 0) {
- return K_MIN;
- }
- if (k.compareTo(K_MAX) > 0) {
- return K_MAX;
- }
- return k;
- }
- */
- public static String formatDate(LocalDate date) {
- return date.format(DATE_FMT);
- }
- private static final BigDecimal DECLARED_CAPACITY_DIVISOR = new BigDecimal("2");
- private static final BigDecimal MINUTES_PER_HOUR = new BigDecimal("60");
- /**
- * 响应执行时段小时数(执行开始至结束)。
- */
- public static BigDecimal responseWindowHours(LocalDateTime windowStart, LocalDateTime windowEnd) {
- if (windowStart == null || windowEnd == null || windowEnd.isBefore(windowStart)) {
- return BigDecimal.ZERO;
- }
- long minutes = java.time.Duration.between(windowStart, windowEnd).toMinutes();
- return BigDecimal.valueOf(minutes).divide(MINUTES_PER_HOUR, 4, RoundingMode.HALF_UP);
- }
- /**
- * 响应申报容量 = 响应时段内历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。
- */
- public static BigDecimal calculateDeclaredCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
- LocalDateTime windowStart,
- LocalDateTime windowEnd,
- BigDecimal correctionFactorK) {
- BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
- if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
- return BigDecimal.ZERO;
- }
- return applyDeclaredCapacityFormula(historicalAvg, correctionFactorK, windowStart, windowEnd);
- }
- /**
- * 预估响应容量 = 响应时段内今日负荷均值。
- */
- public static BigDecimal calculateEstimatedResponseCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
- LocalDateTime windowStart,
- LocalDateTime windowEnd) {
- BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
- if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
- return BigDecimal.ZERO;
- }
- return historicalAvg;
- }
- private static BigDecimal applyDeclaredCapacityFormula(BigDecimal historicalAvg,
- BigDecimal correctionFactorK,
- LocalDateTime windowStart,
- LocalDateTime windowEnd) {
- BigDecimal k = correctionFactorK != null ? correctionFactorK : K_DEFAULT;
- BigDecimal windowHours = responseWindowHours(windowStart, windowEnd);
- return scale(historicalAvg.multiply(k).multiply(windowHours)
- .divide(DECLARED_CAPACITY_DIVISOR, 6, RoundingMode.HALF_UP));
- }
- public static List<DeclaredCapacityPointVO> buildDeclaredCapacityPoints(Map<String, BigDecimal> historicalAvgByTime,
- LocalDateTime windowStart,
- LocalDateTime windowEnd,
- BigDecimal correctionFactorK) {
- List<DeclaredCapacityPointVO> points = new ArrayList<>();
- if (historicalAvgByTime == null || historicalAvgByTime.isEmpty()) {
- return points;
- }
- LocalDate date = windowStart.toLocalDate();
- for (Map.Entry<String, BigDecimal> entry : historicalAvgByTime.entrySet()) {
- LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
- if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
- continue;
- }
- BigDecimal historicalAvg = entry.getValue() != null ? entry.getValue() : BigDecimal.ZERO;
- DeclaredCapacityPointVO point = new DeclaredCapacityPointVO();
- point.setTime(entry.getKey());
- point.setHistoricalAvgKw(scale(historicalAvg));
- point.setDeclaredCapacityKw(applyDeclaredCapacityFormula(
- historicalAvg, correctionFactorK, windowStart, windowEnd));
- points.add(point);
- }
- return points;
- }
- /*
- public static BigDecimal mockDeclaredCapacityKw(Long siteId,
- LocalDate responseDate,
- LocalDateTime windowStart,
- LocalDateTime windowEnd,
- BigDecimal correctionFactorK) {
- return calculateDeclaredCapacityKw(
- buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
- windowStart,
- windowEnd,
- correctionFactorK);
- }
- public static List<DeclaredCapacityPointVO> buildMockDeclaredCapacityPoints(Long siteId,
- LocalDate responseDate,
- LocalDateTime windowStart,
- LocalDateTime windowEnd,
- BigDecimal correctionFactorK) {
- return buildDeclaredCapacityPoints(
- buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
- windowStart,
- windowEnd,
- correctionFactorK);
- }
- private static Map<String, BigDecimal> buildMockHistoricalWindow(Long siteId,
- LocalDate responseDate,
- LocalDateTime windowStart,
- LocalDateTime windowEnd) {
- Map<String, BigDecimal> mockHistorical = new TreeMap<>();
- long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
- LocalDateTime cursor = windowStart;
- while (!cursor.isAfter(windowEnd)) {
- double hourFactor = mockHourFactor(cursor.toLocalTime());
- double noise = 0.9 + pseudoRandom(seed, cursor.getHour() * 60 + cursor.getMinute(), 4) * 0.2;
- mockHistorical.put(cursor.toLocalTime().format(TIME_FMT),
- BigDecimal.valueOf(900 * hourFactor * noise).setScale(2, RoundingMode.HALF_UP));
- cursor = cursor.plusMinutes(INTERVAL_MINUTES);
- }
- return mockHistorical;
- }
- */
- private static BigDecimal averageValuesInWindow(Map<String, BigDecimal> valuesByTime,
- LocalDateTime windowStart,
- LocalDateTime windowEnd) {
- if (valuesByTime == null || valuesByTime.isEmpty()) {
- return null;
- }
- List<BigDecimal> values = new ArrayList<>();
- LocalDate date = windowStart.toLocalDate();
- for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
- LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
- if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
- continue;
- }
- if (entry.getValue() != null) {
- values.add(entry.getValue());
- }
- }
- return values.isEmpty() ? null : average(values);
- }
- private static Map<String, List<BigDecimal>> bucketSeries(TreeMap<LocalDateTime, BigDecimal> series,
- int intervalMinutes) {
- Map<String, List<BigDecimal>> buckets = new TreeMap<>();
- for (Map.Entry<LocalDateTime, BigDecimal> entry : series.entrySet()) {
- if (entry.getValue() == null) {
- continue;
- }
- String bucket = toBucketKey(entry.getKey(), intervalMinutes);
- buckets.computeIfAbsent(bucket, key -> new ArrayList<>()).add(entry.getValue());
- }
- return buckets;
- }
- private static String toBucketKey(LocalDateTime timestamp, int intervalMinutes) {
- int totalMinutes = timestamp.getHour() * 60 + timestamp.getMinute();
- int bucketMinutes = (totalMinutes / intervalMinutes) * intervalMinutes;
- return LocalTime.of(bucketMinutes / 60, bucketMinutes % 60).format(TIME_FMT);
- }
- private static LocalTime parseTimeKey(String timeKey) {
- if (timeKey.length() == 5) {
- return LocalTime.parse(timeKey + ":00", TIME_WITH_SEC_FMT);
- }
- return LocalTime.parse(timeKey, TIME_WITH_SEC_FMT);
- }
- private static BigDecimal sumValuesInWindow(Map<String, BigDecimal> valuesByTime,
- LocalDateTime windowStart,
- LocalDateTime windowEnd) {
- if (valuesByTime == null || valuesByTime.isEmpty()) {
- return null;
- }
- BigDecimal total = BigDecimal.ZERO;
- int count = 0;
- LocalDate date = windowStart.toLocalDate();
- for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
- LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
- if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
- continue;
- }
- if (entry.getValue() != null) {
- total = total.add(entry.getValue());
- count++;
- }
- }
- return count == 0 ? null : total;
- }
- private static TreeMap<LocalDateTime, BigDecimal> pickSeries(Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap) {
- if (metricMap == null || metricMap.isEmpty()) {
- return null;
- }
- for (String metric : POWER_METRICS) {
- TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
- if (series != null && !series.isEmpty()) {
- return series;
- }
- }
- for (TreeMap<LocalDateTime, BigDecimal> series : metricMap.values()) {
- if (series != null && !series.isEmpty()) {
- return series;
- }
- }
- return null;
- }
- private static BigDecimal sum(List<BigDecimal> values) {
- if (values == null || values.isEmpty()) {
- return BigDecimal.ZERO;
- }
- BigDecimal total = BigDecimal.ZERO;
- for (BigDecimal value : values) {
- if (value != null) {
- total = total.add(value);
- }
- }
- return total;
- }
- private static BigDecimal average(List<BigDecimal> values) {
- if (values == null || values.isEmpty()) {
- return BigDecimal.ZERO;
- }
- return sum(values).divide(BigDecimal.valueOf(values.size()), 4, RoundingMode.HALF_UP);
- }
- private static BigDecimal scale(BigDecimal value) {
- if (value == null) {
- return null;
- }
- return value.setScale(2, RoundingMode.HALF_UP);
- }
- /*
- private static double mockHourFactor(LocalTime time) {
- int hour = time.getHour();
- int minute = time.getMinute();
- double h = hour + minute / 60.0;
- if (h < 6) {
- return 0.45 + h / 6 * 0.15;
- }
- if (h < 9) {
- return 0.6 + (h - 6) / 3 * 0.35;
- }
- if (h < 12) {
- return 0.95 + (h - 9) / 3 * 0.05;
- }
- if (h < 14) {
- return 0.85;
- }
- if (h < 17) {
- return 0.9 + (h - 14) / 3 * 0.1;
- }
- if (h < 21) {
- return 1.0 - (h - 17) / 4 * 0.35;
- }
- return 0.5 - (h - 21) / 3 * 0.15;
- }
- private static double pseudoRandom(long seed, int minuteOfDay, int seriesSalt) {
- long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
- return (mixed % 1000) / 1000.0;
- }
- */
- }
|