Procházet zdrojové kódy

[1012] 增加功率预测数据mock: MockDataTask定时生成超短期(15min/16点)/短期(24文件/288点)/中期(0点/12点/960点)预测CSV, 功率按物理规律模拟

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
taosj před 2 týdny
rodič
revize
49ca7d1cc9

+ 255 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/task/MockDataTask.java

@@ -0,0 +1,255 @@
+package com.ufo.project.data.task;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.Random;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import com.ufo.base.framework.config.ProjectConfig;
+
+/**
+ * 功率预测数据mock生成任务
+ *
+ * 在 project.profile 目录下生成 超短期/短期/中期 功率预测mock文件(生产环境即 /home/power/upload/power/MOCK/BHWP01 下),
+ * 供数据文件同步任务(data_file_sync_config)消费.
+ * 文件命名与init_time均使用UTC时间; time列为UTC, time_shanghai列为UTC+8.
+ * 文件格式:
+ * <pre>
+ * init_time,time,time_shanghai,power
+ * 2602281600,2026-02-28T16:15Z,2026-03-01 00:15:00,0.000
+ * </pre>
+ * power为尽量符合物理规律的光伏出力mock: 夜间为0, 白天呈钟形曲线,
+ * 日出日落与峰值随季节变化(夏长夏高/冬短冬低), 叠加缓慢的云量波动;
+ * 随机参数由起报时间派生, 同一文件的生成结果稳定可复现.
+ *
+ * @author ufo
+ * @date 2026-10-21
+ */
+@Component
+public class MockDataTask
+{
+    private static final Logger log = LoggerFactory.getLogger(MockDataTask.class);
+
+    /** UTC时区 */
+    private static final ZoneOffset UTC = ZoneOffset.UTC;
+
+    /** 上海时区(UTC+8) */
+    private static final ZoneOffset SHANGHAI = ZoneOffset.ofHours(8);
+
+    /** time列格式(UTC, 如 2026-02-28T16:15Z) */
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'");
+
+    /** time_shanghai列格式 */
+    private static final DateTimeFormatter TIME_SH_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /** 超短期文件名时间格式(yyyyMMddHHmm) */
+    private static final DateTimeFormatter ULTRA_FILE_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
+
+    /** 超短期init_time格式(yyMMddHHmm) */
+    private static final DateTimeFormatter ULTRA_INIT_FMT = DateTimeFormatter.ofPattern("yyMMddHHmm");
+
+    /** 短期/中期文件名时间格式(yyyyMMddHH) */
+    private static final DateTimeFormatter TERM_FILE_FMT = DateTimeFormatter.ofPattern("yyyyMMddHH");
+
+    /** 短期/中期init_time格式(yyMMddHH) */
+    private static final DateTimeFormatter TERM_INIT_FMT = DateTimeFormatter.ofPattern("yyMMddHH");
+
+    /** 超短期数据点数(起报时间+15分钟起4小时, 共16点) */
+    private static final int ULTRA_POINTS = 16;
+
+    /** 短期数据点数(自起报时间(含)起288点) */
+    private static final int SHORT_POINTS = 288;
+
+    /** 中期数据点数(自起报时间(含)起960点) */
+    private static final int MEDIUM_POINTS = 960;
+
+    /** 出力上限 */
+    private static final double POWER_CAP = 2600.0;
+
+    /**
+     * 超短期: 每15分钟执行一次, 以当前15分钟整点(0/15/30/45)为起报时间生成预报文件,
+     * 数据自起报时间+15分钟至起报时间+4小时, 共16点, 间隔15分钟
+     */
+    @Scheduled(cron = "0 0/15 * * * ?")
+    public void generateUltraShortTerm()
+    {
+        ZonedDateTime now = ZonedDateTime.now(UTC).withSecond(0).withNano(0);
+        ZonedDateTime init = now.minusMinutes(now.getMinute() % 15);
+        try
+        {
+            Path file = generateFile("UltraShortTerm", init, ULTRA_FILE_FMT, ULTRA_INIT_FMT, 15, ULTRA_POINTS);
+            log.info("[MockDataTask] 生成超短期mock文件, file={}", file);
+        }
+        catch (Exception e)
+        {
+            log.error("[MockDataTask] 生成超短期mock文件失败, init={}", init, e);
+        }
+    }
+
+    /**
+     * 短期: 每天3点执行一次, 生成当日全部24个文件(起报时间为当日UTC 00/01/.../23时),
+     * 每文件288点, 自起报时间(含)起, 间隔15分钟
+     */
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void generateShortTerm()
+    {
+        ZonedDateTime day0 = ZonedDateTime.now(UTC).toLocalDate().atStartOfDay(UTC);
+        for (int hour = 0; hour < 24; hour++)
+        {
+            ZonedDateTime init = day0.plusHours(hour);
+            try
+            {
+                Path file = generateFile("ShortTerm", init, TERM_FILE_FMT, TERM_INIT_FMT, 0, SHORT_POINTS);
+                log.info("[MockDataTask] 生成短期mock文件, file={}", file);
+            }
+            catch (Exception e)
+            {
+                log.error("[MockDataTask] 生成短期mock文件失败, init={}", init, e);
+            }
+        }
+    }
+
+    /**
+     * 中期: 每天3点执行一次, 生成当日0点数据
+     */
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void generateMediumTermMidnight()
+    {
+        generateMediumTermFile(0);
+    }
+
+    /**
+     * 中期: 每天11点执行一次, 生成当日12点数据
+     */
+    @Scheduled(cron = "0 0 11 * * ?")
+    public void generateMediumTermNoon()
+    {
+        generateMediumTermFile(12);
+    }
+
+    /**
+     * 生成单个中期mock文件(960点, 自起报时间(含)起, 间隔15分钟)
+     *
+     * @param hour 起报时间(当日UTC小时)
+     */
+    private void generateMediumTermFile(int hour)
+    {
+        ZonedDateTime init = ZonedDateTime.now(UTC).toLocalDate().atStartOfDay(UTC).plusHours(hour);
+        try
+        {
+            Path file = generateFile("MediumTerm", init, TERM_FILE_FMT, TERM_INIT_FMT, 0, MEDIUM_POINTS);
+            log.info("[MockDataTask] 生成中期mock文件, file={}", file);
+        }
+        catch (Exception e)
+        {
+            log.error("[MockDataTask] 生成中期mock文件失败, init={}", init, e);
+        }
+    }
+
+    /**
+     * 生成单个mock预报文件
+     *
+     * @param type           类型目录(UltraShortTerm/ShortTerm/MediumTerm)
+     * @param init           起报时间(UTC)
+     * @param fileFmt        文件名时间格式
+     * @param initFmt        init_time格式
+     * @param firstOffsetMin 首个数据点相对起报时间的偏移(分钟)
+     * @param points         数据点数
+     * @return 生成的文件路径
+     */
+    private Path generateFile(String type, ZonedDateTime init, DateTimeFormatter fileFmt,
+            DateTimeFormatter initFmt, int firstOffsetMin, int points) throws IOException
+    {
+        Path dir = Paths.get(ProjectConfig.getProfile(), "power", "MOCK", "BHWP01", type,
+                String.format(Locale.ROOT, "%04d", init.getYear()),
+                String.format(Locale.ROOT, "%02d", init.getMonthValue()),
+                String.format(Locale.ROOT, "%02d", init.getDayOfMonth()));
+        Path file = dir.resolve(init.format(fileFmt) + ".csv");
+        Files.createDirectories(dir);
+
+        Random random = new Random(init.toInstant().toEpochMilli() / 60000L);
+        double t0 = init.toInstant().getEpochSecond() / 3600.0;
+        double a1 = 0.15 + 0.30 * random.nextDouble();
+        double a2 = 0.02 + 0.06 * random.nextDouble();
+        double p1 = 2 * Math.PI * random.nextDouble();
+        double p2 = 2 * Math.PI * random.nextDouble();
+        double t1 = 5 + 2 * random.nextDouble();
+        double t2 = 1.0 + 1.0 * random.nextDouble();
+
+        StringBuilder sb = new StringBuilder(64 + points * 52);
+        sb.append("init_time,time,time_shanghai,power\n");
+        String initTime = init.format(initFmt);
+        for (int i = 0; i < points; i++)
+        {
+            ZonedDateTime t = init.plusMinutes((long) firstOffsetMin + 15L * i);
+            sb.append(initTime).append(',')
+                    .append(t.format(TIME_FMT)).append(',')
+                    .append(t.withZoneSameInstant(SHANGHAI).format(TIME_SH_FMT)).append(',')
+                    .append(String.format(Locale.ROOT, "%.3f",
+                            mockPower(t, t0, a1, p1, t1, a2, p2, t2)))
+                    .append('\n');
+        }
+        Files.write(file, sb.toString().getBytes(StandardCharsets.UTF_8),
+                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
+        return file;
+    }
+
+    /**
+     * 计算mock光伏出力: 夜间为0, 白天钟形曲线, 峰值与日照时长随季节变化, 叠加缓慢云量波动
+     *
+     * @param tUtc 数据时刻(UTC)
+     * @param t0   起报时间(epoch小时), 用于云量相位基准
+     * @param a1   慢周期云量幅值
+     * @param p1   慢周期云量相位
+     * @param t1   慢周期云量周期(小时)
+     * @param a2   快周期云量幅值
+     * @param p2   快周期云量相位
+     * @param t2   快周期云量周期(小时)
+     * @return 出力值
+     */
+    private double mockPower(ZonedDateTime tUtc, double t0, double a1, double p1, double t1,
+            double a2, double p2, double t2)
+    {
+        int doy = tUtc.toLocalDate().getDayOfYear();
+        double season = Math.cos(2 * Math.PI * (doy - 195.0) / 365.0);
+        double peak = 2000 + 600 * season;
+        double halfDay = 5.9 + 1.4 * season;
+        double sunrise = 12 - halfDay;
+        double sunset = 12 + halfDay;
+
+        ZonedDateTime local = tUtc.withZoneSameInstant(SHANGHAI);
+        double hour = local.getHour() + local.getMinute() / 60.0;
+        if (hour <= sunrise || hour >= sunset)
+        {
+            return 0.0;
+        }
+        double solar = Math.pow(Math.sin(Math.PI * (hour - sunrise) / (sunset - sunrise)), 1.3);
+
+        double tHours = tUtc.toInstant().getEpochSecond() / 3600.0;
+        double cloud1 = 1 - a1 * (0.5 + 0.5 * Math.sin(2 * Math.PI * (tHours - t0) / t1 + p1));
+        double cloud2 = 1 - a2 * (0.5 + 0.5 * Math.sin(2 * Math.PI * (tHours - t0) / t2 + p2));
+
+        double power = peak * solar * cloud1 * cloud2;
+        if (power < 0)
+        {
+            power = 0;
+        }
+        if (power > POWER_CAP)
+        {
+            power = POWER_CAP;
+        }
+        return power;
+    }
+}