|
|
@@ -6,6 +6,7 @@ import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.nio.file.Paths;
|
|
|
import java.nio.file.StandardOpenOption;
|
|
|
+import java.time.LocalDate;
|
|
|
import java.time.ZoneOffset;
|
|
|
import java.time.ZonedDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
@@ -20,9 +21,10 @@ import org.springframework.stereotype.Component;
|
|
|
import com.ufo.base.framework.config.ProjectConfig;
|
|
|
|
|
|
/**
|
|
|
- * 功率预测数据mock生成任务
|
|
|
+ * 功率与气象mock数据生成任务
|
|
|
*
|
|
|
- * 在 project.profile 目录下生成 超短期/短期/中期 功率预测mock文件(生产环境即 /home/power/upload/power/MOCK/BHWP01 下),
|
|
|
+ * 在 project.profile 目录下生成 超短期/短期/中期 功率预测mock文件(生产环境即 /home/power/upload/power/MOCK/BHWP01 下)
|
|
|
+ * 及短期气象mock文件(生产环境即 /home/power/upload/weather/MOCK/BHWP01 下),
|
|
|
* 供数据文件同步任务(data_file_sync_config)消费.
|
|
|
* 文件命名与init_time均使用UTC时间; time列为UTC, time_shanghai列为UTC+8.
|
|
|
* 文件格式:
|
|
|
@@ -34,6 +36,10 @@ import com.ufo.base.framework.config.ProjectConfig;
|
|
|
* 日出日落与峰值随季节变化(夏长夏高/冬短冬低), 叠加缓慢的云量波动;
|
|
|
* 随机参数由起报时间派生, 同一文件的生成结果稳定可复现.
|
|
|
*
|
|
|
+ * 短期气象mock为SYSU格式38列CSV, 自起报时间(含)起961点(10天), 间隔15分钟,
|
|
|
+ * 文件名时间为UTC, date_time列为GMT+8(即文件名时间+8h);
|
|
|
+ * t2m/rh2/sp/云量/风/ghi/dni/dhi尽量符合物理规律, 按北纬38.9度定标, 同样按起报时间可复现.
|
|
|
+ *
|
|
|
* @author ufo
|
|
|
* @date 2026-10-21
|
|
|
*/
|
|
|
@@ -78,6 +84,32 @@ public class MockDataTask
|
|
|
/** 出力上限 */
|
|
|
private static final double POWER_CAP = 2600.0;
|
|
|
|
|
|
+ /** 短期气象数据点数(自起报时间(含)起960个间隔, 961点) */
|
|
|
+ private static final int WEATHER_POINTS = 961;
|
|
|
+
|
|
|
+ /** 气象mock场站编号(farm_id, 同时是文件名的场站段) */
|
|
|
+ private static final String WEATHER_FARM_ID = "BHWP01";
|
|
|
+
|
|
|
+ /** 气象mock固定经度(文件名按经度-纬度顺序) */
|
|
|
+ private static final double WEATHER_LON = 121.4737;
|
|
|
+
|
|
|
+ /** 气象mock固定纬度 */
|
|
|
+ private static final double WEATHER_LAT = 38.9087;
|
|
|
+
|
|
|
+ /** 气象文件名日期格式(yyyyMMdd, UTC即北京日期) */
|
|
|
+ private static final DateTimeFormatter WEATHER_FILE_DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
|
+
|
|
|
+ /** 气象date_time列格式(GMT+8) */
|
|
|
+ private static final DateTimeFormatter WEATHER_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
|
|
|
+
|
|
|
+ /** 风速/风向高度序列(m) */
|
|
|
+ private static final int[] WEATHER_HEIGHTS = {10, 30, 50, 70, 80, 90, 100, 120, 140, 160, 180, 200};
|
|
|
+
|
|
|
+ /** 气象文件表头(与真实SYSU文件一致, 38列) */
|
|
|
+ private static final String WEATHER_HEADER = "farm_id,date_time,lat,lon,sp,t2m,rh2,tcc,lcc,mcc,hcc,"
|
|
|
+ + "spd10,spd30,spd50,spd70,spd80,spd90,spd100,spd120,spd140,spd160,spd180,spd200,"
|
|
|
+ + "dir10,dir30,dir50,dir70,dir80,dir90,dir100,dir120,dir140,dir160,dir180,dir200,ghi,dni,dhi";
|
|
|
+
|
|
|
/**
|
|
|
* 超短期: 每15分钟执行一次, 以当前15分钟整点(0/15/30/45)为起报时间生成预报文件,
|
|
|
* 数据自起报时间+15分钟至起报时间+4小时, 共16点, 间隔15分钟
|
|
|
@@ -158,6 +190,26 @@ public class MockDataTask
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 短期气象: 每天凌晨2点执行一次, 生成当日(北京日期)12:00 UTC起报的一个预报文件,
|
|
|
+ * 自起报时间(含)起961点, 间隔15分钟, 输出到 weather/MOCK/BHWP01/;
|
|
|
+ * 文件名时间为UTC, date_time列为GMT+8(即文件名时间+8h)
|
|
|
+ */
|
|
|
+ @Scheduled(cron = "0 0 2 * * ?")
|
|
|
+ public void generateShortTermWeather()
|
|
|
+ {
|
|
|
+ ZonedDateTime init = LocalDate.now(SHANGHAI).atTime(12, 0).atZone(UTC);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ Path file = generateWeatherFile(init);
|
|
|
+ log.info("[MockDataTask] 生成短期气象mock文件, file={}", file);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ log.error("[MockDataTask] 生成短期气象mock文件失败, init={}", init, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成单个mock预报文件
|
|
|
*
|
|
|
@@ -252,4 +304,152 @@ public class MockDataTask
|
|
|
}
|
|
|
return power;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成短期气象mock文件(SYSU格式, 38列, 961点, 间隔15分钟)
|
|
|
+ *
|
|
|
+ * @param init 起报时间(12:00 UTC)
|
|
|
+ * @return 生成的文件路径
|
|
|
+ */
|
|
|
+ private Path generateWeatherFile(ZonedDateTime init) throws IOException
|
|
|
+ {
|
|
|
+ Path dir = Paths.get(ProjectConfig.getProfile(), "weather", "MOCK", WEATHER_FARM_ID);
|
|
|
+ String lon = String.format(Locale.ROOT, "%.4f", WEATHER_LON);
|
|
|
+ String lat = String.format(Locale.ROOT, "%.4f", WEATHER_LAT);
|
|
|
+ Path file = dir.resolve(String.format(Locale.ROOT, "%s_%s-%s_%s12_SYSU.csv",
|
|
|
+ WEATHER_FARM_ID, lon, lat, init.format(WEATHER_FILE_DATE_FMT)));
|
|
|
+ Files.createDirectories(dir);
|
|
|
+
|
|
|
+ Random random = new Random(init.toInstant().toEpochMilli() / 60000L);
|
|
|
+ double t0 = init.toInstant().getEpochSecond() / 3600.0;
|
|
|
+
|
|
|
+ // 每文件随机参数(由起报时间派生, 生成结果稳定可复现)
|
|
|
+ double baseCc = 20 + 40 * random.nextDouble();
|
|
|
+ double ampCc = 10 + 20 * random.nextDouble();
|
|
|
+ double tCc = 48 + 24 * random.nextDouble();
|
|
|
+ double pCc = 2 * Math.PI * random.nextDouble();
|
|
|
+ double rM = 0.4 + 0.3 * random.nextDouble();
|
|
|
+ double rL = (1 - rM) * (0.5 + 0.4 * random.nextDouble());
|
|
|
+ double rH = 1 - rM - rL;
|
|
|
+ double tP = 96 + 48 * random.nextDouble();
|
|
|
+ double pP = 2 * Math.PI * random.nextDouble();
|
|
|
+ double windBase = 3 + 2 * random.nextDouble();
|
|
|
+ double pW = 2 * Math.PI * random.nextDouble();
|
|
|
+ double alpha = 0.20 + 0.08 * random.nextDouble();
|
|
|
+ double dirBase = 360 * random.nextDouble();
|
|
|
+ double tD = 48 + 48 * random.nextDouble();
|
|
|
+ double pD = 2 * Math.PI * random.nextDouble();
|
|
|
+ double tD2 = 6 + 8 * random.nextDouble();
|
|
|
+ double pD2 = 2 * Math.PI * random.nextDouble();
|
|
|
+ double[] dirOffsets = new double[WEATHER_HEIGHTS.length];
|
|
|
+ for (int i = 0; i < dirOffsets.length; i++)
|
|
|
+ {
|
|
|
+ dirOffsets[i] = (random.nextDouble() - 0.5) * 8;
|
|
|
+ }
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder(64 + WEATHER_POINTS * 160);
|
|
|
+ sb.append(WEATHER_HEADER).append('\n');
|
|
|
+
|
|
|
+ for (int i = 0; i < WEATHER_POINTS; i++)
|
|
|
+ {
|
|
|
+ ZonedDateTime t = init.plusMinutes(15L * i);
|
|
|
+ ZonedDateTime local = t.withZoneSameInstant(SHANGHAI);
|
|
|
+ double hour = local.getHour() + local.getMinute() / 60.0;
|
|
|
+ double tHours = t.toInstant().getEpochSecond() / 3600.0;
|
|
|
+
|
|
|
+ // 太阳: 日出日落随季节变化, cosz近似太阳高度正弦
|
|
|
+ int doy = t.toLocalDate().getDayOfYear();
|
|
|
+ double season = Math.cos(2 * Math.PI * (doy - 195.0) / 365.0);
|
|
|
+ double halfDay = 5.9 + 1.4 * season;
|
|
|
+ double sunrise = 12 - halfDay;
|
|
|
+ double sunset = 12 + halfDay;
|
|
|
+ double cosz = 0;
|
|
|
+ if (hour > sunrise && hour < sunset)
|
|
|
+ {
|
|
|
+ cosz = Math.sin(Math.PI * (hour - sunrise) / (sunset - sunrise));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 云量: 总云量慢漂移, 按比例拆三层(和等于总云量)
|
|
|
+ double tcc = clamp(baseCc + ampCc * Math.sin(2 * Math.PI * (tHours - t0) / tCc + pCc), 0, 100);
|
|
|
+ double cloud = tcc / 100.0;
|
|
|
+
|
|
|
+ // 温度/湿度/气压
|
|
|
+ double t2m = 17 + 7 * season + 5 * Math.cos(2 * Math.PI * (hour - 14.5) / 24);
|
|
|
+ double rh2 = clamp(85 - 2.2 * t2m + 12 * cloud, 35, 98);
|
|
|
+ double sp = 101300 + 300 * Math.sin(2 * Math.PI * (tHours - t0) / tP + pP);
|
|
|
+
|
|
|
+ // 风: 10m风速日变化, 幂律廓线, 风向双正弦摆动
|
|
|
+ double spd10 = Math.max(0.3, windBase + Math.sin(2 * Math.PI * (tHours - t0) / 24 + pW));
|
|
|
+ double dir10 = wrapDir(dirBase
|
|
|
+ + 40 * Math.sin(2 * Math.PI * (tHours - t0) / tD + pD)
|
|
|
+ + 12 * Math.sin(2 * Math.PI * (tHours - t0) / tD2 + pD2));
|
|
|
+
|
|
|
+ // 辐照: 晴空峰值随季节, 云量主要削弱直分量, 散射随云量增大, 夜间均为0
|
|
|
+ double ghi = 0, dni = 0, dhi = 0;
|
|
|
+ if (cosz > 0)
|
|
|
+ {
|
|
|
+ double peak = 750 + 100 * season;
|
|
|
+ dni = 0.90 * peak * (1 - 0.95 * cloud);
|
|
|
+ dhi = 0.10 * peak * cosz * (1 + cloud);
|
|
|
+ ghi = dni * cosz + dhi;
|
|
|
+ }
|
|
|
+
|
|
|
+ sb.append(WEATHER_FARM_ID).append(',')
|
|
|
+ .append(local.format(WEATHER_TIME_FMT)).append(',')
|
|
|
+ .append(lat).append(',')
|
|
|
+ .append(lon).append(',')
|
|
|
+ .append(fmt3(sp)).append(',')
|
|
|
+ .append(fmt3(t2m)).append(',')
|
|
|
+ .append(fmt3(rh2)).append(',')
|
|
|
+ .append(fmt1(tcc)).append(',')
|
|
|
+ .append(fmt1(tcc * rL)).append(',')
|
|
|
+ .append(fmt1(tcc * rM)).append(',')
|
|
|
+ .append(fmt1(tcc * rH)).append(',');
|
|
|
+ for (int h = 0; h < WEATHER_HEIGHTS.length; h++)
|
|
|
+ {
|
|
|
+ if (h > 0)
|
|
|
+ {
|
|
|
+ sb.append(',');
|
|
|
+ }
|
|
|
+ sb.append(fmt3(spd10 * Math.pow(WEATHER_HEIGHTS[h] / 10.0, alpha)));
|
|
|
+ }
|
|
|
+ sb.append(',');
|
|
|
+ for (int h = 0; h < WEATHER_HEIGHTS.length; h++)
|
|
|
+ {
|
|
|
+ if (h > 0)
|
|
|
+ {
|
|
|
+ sb.append(',');
|
|
|
+ }
|
|
|
+ sb.append(fmt3(wrapDir(dir10 + dirOffsets[h])));
|
|
|
+ }
|
|
|
+ sb.append(',')
|
|
|
+ .append(fmt3(ghi)).append(',')
|
|
|
+ .append(fmt3(dni)).append(',')
|
|
|
+ .append(fmt3(dhi)).append('\n');
|
|
|
+ }
|
|
|
+ Files.write(file, sb.toString().getBytes(StandardCharsets.UTF_8),
|
|
|
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String fmt3(double v)
|
|
|
+ {
|
|
|
+ return String.format(Locale.ROOT, "%.3f", v);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String fmt1(double v)
|
|
|
+ {
|
|
|
+ return String.format(Locale.ROOT, "%.1f", v);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double clamp(double v, double min, double max)
|
|
|
+ {
|
|
|
+ return Math.max(min, Math.min(max, v));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double wrapDir(double d)
|
|
|
+ {
|
|
|
+ double r = d % 360;
|
|
|
+ return r < 0 ? r + 360 : r;
|
|
|
+ }
|
|
|
}
|