|
|
@@ -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;
|
|
|
+ }
|
|
|
+}
|