瀏覽代碼

[2610020] 增加短期气象mock: MockDataTask 新增每天2点执行的 generateShortTermWeather(起报=当日北京日期12:00UTC), 输出 SYSU格式38列CSV(961点/15分钟/10天)到 weather/MOCK/BHWP01/, 文件名UTC+date_time列GMT+8, t2m/rh2/sp/云量/风/辐照按北纬38.9度物理模型生成且按起报时间可复现

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
taosj 1 周之前
父節點
當前提交
bb812b99a1
共有 1 個文件被更改,包括 202 次插入 和 2 次删除
  1. 202 2
      forecast-backend-server/src/main/java/com/ufo/project/data/task/MockDataTask.java

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

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