prediction_test.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import numpy as np
  2. import pandas as pd
  3. import torch
  4. import os
  5. from datetime import datetime, timedelta
  6. import sys
  7. import argparse
  8. import posixpath
  9. import getpass
  10. import paramiko
  11. weather_utils_path = os.path.abspath(
  12. os.path.join(os.path.dirname(__file__), "..")
  13. )
  14. if weather_utils_path not in sys.path:
  15. sys.path.insert(0, weather_utils_path)
  16. from scripts.ModelLoading import ModelLoading,SolorFeature
  17. _LOCAL_COLS = ("time_shanghai", "time_local", "time_bj", "时间戳")
  18. def parse_init_time(init_time) -> pd.Timestamp:
  19. """起报时间 -> tz-aware UTC Timestamp。支持 'YYMMDDHH' / 'YYYYMMDDHH' / ISO / datetime / Timestamp"""
  20. if isinstance(init_time, pd.Timestamp):
  21. ts = init_time
  22. elif hasattr(init_time, "year") and not isinstance(init_time, str):
  23. ts = pd.Timestamp(init_time)
  24. else:
  25. s = str(init_time).strip()
  26. digits = "".join(ch for ch in s if ch.isdigit())
  27. if len(s) == 8 and s.isdigit(): # YYMMDDHH
  28. ts = pd.to_datetime("20" + s, format="%Y%m%d%H")
  29. elif len(digits) == 10 and s.isdigit(): # YYYYMMDDHH
  30. ts = pd.to_datetime(s, format="%Y%m%d%H")
  31. elif "T" in s or " " in s or "-" in s:
  32. ts = pd.to_datetime(s, utc=True).tz_localize(None)
  33. else:
  34. raise ValueError(f"无法识别的 init_time: {init_time!r}")
  35. ts = pd.Timestamp(ts)
  36. return ts.tz_localize("UTC") if ts.tz is None else ts.tz_convert("UTC")
  37. def build_forecast_df(shortTerm_result, init_time, *, freq="15min", tz="Asia/Shanghai",
  38. pred_col="power", init_fmt="%y%m%d%H",
  39. time_fmt="%Y-%m-%dT%H:%MZ", local_fmt="%Y-%m-%d %H:%M:%S"):
  40. """
  41. shortTerm_result : 模型输出, 自动 reshape(-1); 长度 = 预报步数(短期 288 / 中期 960)
  42. init_time : 起报时间(UTC)
  43. 返回: DataFrame, 列顺序 init_time(YYMMDDHH) | time(UTC) | time_shanghai | <pred_col>
  44. 时间从起报时刻起, 共 N 个点, 步长 15min
  45. """
  46. pred = np.asarray(shortTerm_result).reshape(-1)
  47. n = pred.size
  48. if n == 0:
  49. raise ValueError("shortTerm_result 为空")
  50. t0 = parse_init_time(init_time)
  51. utc = pd.date_range(t0, periods=n, freq=freq)
  52. return pd.DataFrame({
  53. "init_time": [t0.strftime(init_fmt)] * n,
  54. "time": utc.strftime(time_fmt),
  55. "time_shanghai": utc.tz_convert(tz).strftime(local_fmt),
  56. pred_col: np.asarray(pred, dtype=float),
  57. })
  58. def remove_night_power(MediumTerm, lat, lon, *, power_col="power", time_col="time",
  59. elev_min=0.0, tz="Asia/Shanghai", clip_negative=True, return_info=False):
  60. """
  61. MediumTerm : DataFrame, 含时间列(默认 'time', UTC)与功率列(默认 'power')
  62. lat, lon : 场站纬度、经度(度), 正东经/正北纬
  63. power_col : 功率列名, 默认 'power'
  64. time_col : 时间列名, 默认 'time'(UTC); 传 'time_shanghai' 按北京时间解释;
  65. 指定列不存在而存在 'time_shanghai' 时自动回退
  66. elev_min : 夜间阈值(度), 默认 0(含大气折射修正, 即视地平线)
  67. clip_negative : 是否同时把负功率置 0
  68. return_info : True 时返回 (df, info)
  69. 返回: 处理后的 DataFrame(副本), 列与原表一致, 仅 power 列被改写
  70. """
  71. if not isinstance(MediumTerm, pd.DataFrame):
  72. raise TypeError("MediumTerm 需要是 DataFrame(含时间列); 若只有数值请先自行补上时间列")
  73. df = MediumTerm.copy()
  74. # 1. 时间列
  75. if time_col not in df.columns:
  76. alt = next((c for c in _LOCAL_COLS if c in df.columns), None)
  77. if alt is None:
  78. raise ValueError(f"时间列 {time_col!r} 不存在, 现有列: {list(df.columns)}")
  79. time_col = alt
  80. t = pd.to_datetime(df[time_col], errors="coerce")
  81. if t.isna().any():
  82. raise ValueError(f"时间列 {time_col!r} 存在无法解析的值({int(t.isna().sum())} 个)")
  83. is_local = (time_col in _LOCAL_COLS) or time_col.lower().endswith("_shanghai")
  84. if getattr(t.dt, "tz", None) is not None:
  85. t_utc = t.dt.tz_convert("UTC") # 已带时区 -> 转 UTC
  86. elif is_local:
  87. t_utc = t.dt.tz_localize(tz).dt.tz_convert("UTC") # time_shanghai(北京时) -> UTC
  88. else:
  89. t_utc = t.dt.tz_localize("UTC") # 'time' 列按 UTC 约定
  90. # 2. 功率列
  91. if power_col not in df.columns:
  92. raise ValueError(f"功率列 {power_col!r} 不在列中: {list(df.columns)}")
  93. # 3. 太阳高度角 -> 夜间掩码
  94. d = pd.DatetimeIndex(t_utc.dt.tz_localize(None))
  95. jd = d.values.astype("datetime64[s]").astype("int64") / 86400.0 + 2440587.5
  96. T = (jd - 2451545.0) / 36525.0
  97. L0 = 280.46646 + T * (36000.76983 + T * 0.0003032)
  98. M = 357.52911 + T * (35999.05029 - 0.0001537 * T)
  99. e = 0.016708634 - T * (0.000042037 + 0.0000001267 * T)
  100. Mr = np.radians(M)
  101. C = (np.sin(Mr) * (1.914602 - T * (0.004817 + 0.000014 * T))
  102. + np.sin(2 * Mr) * (0.019993 - 0.000101 * T) + np.sin(3 * Mr) * 0.000289)
  103. omega = 125.04 - 1934.136 * T
  104. lam = L0 + C - 0.00569 - 0.00478 * np.sin(np.radians(omega))
  105. eps = (23 + (26 + (21.448 - T * (46.815 + T * (0.00059 - T * 0.001813))) / 60) / 60
  106. + 0.00256 * np.cos(np.radians(omega)))
  107. decl = np.degrees(np.arcsin(np.sin(np.radians(eps)) * np.sin(np.radians(lam))))
  108. y = np.tan(np.radians(eps / 2)) ** 2
  109. eot = 4 * np.degrees(y * np.sin(2 * np.radians(L0)) - 2 * e * np.sin(Mr)
  110. + 4 * e * y * np.sin(Mr) * np.cos(2 * np.radians(L0))
  111. - 0.5 * y * y * np.sin(4 * np.radians(L0))
  112. - 1.25 * e * e * np.sin(2 * Mr))
  113. mins = d.hour.values * 60 + d.minute.values + d.second.values / 60.0
  114. ha = np.radians((mins + eot + 4.0 * lon) / 4.0 - 180.0) # 时角(经度正东)
  115. latr, dr = np.radians(lat), np.radians(decl)
  116. cosz = np.sin(latr) * np.sin(dr) + np.cos(latr) * np.cos(dr) * np.cos(ha)
  117. el = 90.0 - np.degrees(np.arccos(np.clip(cosz, -1, 1)))
  118. el = el + np.where(el > -0.575, # 大气折射修正
  119. 1.02 / (60 * np.tan(np.radians(el + 10.3 / (el + 5.11)))), 0.0)
  120. night = el <= elev_min
  121. # 4. 夜间非 0 值置 0
  122. p = pd.to_numeric(df[power_col], errors="coerce")
  123. bad = night & p.notna()
  124. if clip_negative:
  125. bad = bad | (p < 0).fillna(False).to_numpy()
  126. changed = bad & p.notna() & (p.to_numpy() != 0)
  127. info = {"time_col": time_col, "power_col": power_col, "n_rows": len(df),
  128. "n_night": int(night.sum()), "n_night_nonzero": int((night & (p > 0)).sum()),
  129. "n_zeroed": int(changed.sum()), "sum_removed": float(p[changed].sum())}
  130. df[power_col] = p.mask(bad, 0.0)
  131. return (df, info) if return_info else df
  132. def mkdir_p(sftp, remote_dir):
  133. """递归创建远程目录,已存在则跳过"""
  134. dirs = []
  135. current = remote_dir
  136. while current not in ("", ".", "/"):
  137. dirs.append(current)
  138. current = posixpath.dirname(current)
  139. for directory in reversed(dirs):
  140. try:
  141. sftp.stat(directory)
  142. except OSError:
  143. sftp.mkdir(directory)
  144. def upload_files(
  145. host,
  146. port,
  147. username,
  148. local_files,
  149. remote_files,
  150. password=None,
  151. ):
  152. """
  153. 通过 SFTP 批量上传文件。
  154. 参数:
  155. host: SFTP 服务器地址
  156. port: 端口
  157. username: 用户名
  158. local_files: 本地文件路径数组
  159. remote_files: 远程文件路径数组
  160. password: 密码,可选;不传则运行时输入
  161. """
  162. if len(local_files) != len(remote_files):
  163. raise ValueError("local_files 和 remote_files 数量必须一致")
  164. if password is None:
  165. password = getpass.getpass("请输入 SFTP 密码: ")
  166. transport = paramiko.Transport((host, port))
  167. try:
  168. transport.connect(
  169. username=username,
  170. password=password
  171. )
  172. sftp = paramiko.SFTPClient.from_transport(transport)
  173. try:
  174. for local_file, remote_file in zip(local_files, remote_files):
  175. remote_dir = posixpath.dirname(remote_file)
  176. # 自动创建远程目录
  177. mkdir_p(sftp, remote_dir)
  178. print(f"正在上传:")
  179. print(f" 本地: {local_file}")
  180. print(f" 远程: {remote_file}")
  181. sftp.put(
  182. local_file,
  183. remote_file,
  184. confirm=False
  185. )
  186. print("上传成功\n")
  187. finally:
  188. sftp.close()
  189. finally:
  190. transport.close()
  191. if __name__ == '__main__':
  192. # 配置光伏场站信息
  193. parser = argparse.ArgumentParser(description='OuLiPoewev1.0 Prediction')
  194. parser.add_argument('--projs', type=str, default='JMJD01', help='场站名')
  195. parser.add_argument('--model_name', type=str, default='GRU', help='模型名称')
  196. parser.add_argument('--release', type=str, default='v24', help='特征集')
  197. parser.add_argument('--lat', type=float, default=22.292, help='场站纬度')
  198. parser.add_argument('--lon', type=float, default=120.290, help='场站经度')
  199. parser.add_argument('--short_term_seq', type=int, default=288, help='输入序列长度-短期')
  200. parser.add_argument('--medium_term_seq', type=int, default=960, help='输入序列长度-中期')
  201. parser.add_argument('--capacity', type=int, default=4050, help='装机容量(kw)')
  202. parser.add_argument('--saved_model_root', type=str,
  203. default='/home/jinhuaiming/OuLiPowerV1.0/saved_model',
  204. help='模型权重保存根路径')
  205. parser.add_argument('--saved_result_root', type=str,
  206. default='/home/jinhuaiming/OuLiPowerV1.0/archive',
  207. help='预报结果存档根路径')
  208. parser.add_argument('--init_time', type=str, default='2026060200', help='预报时间(UTC)')
  209. args = parser.parse_args()
  210. #=====读取气象数据--气象读取部分=========================================================
  211. # 气象变量特征
  212. # 短期:v24-【'ssrd','tcc','lcc','mcc','hcc','2t','2d','10u','10v'】
  213. # 中期:v24-【'ssrd','tcc','lcc','mcc','hcc','2t','2d','10u','10v'】
  214. # 单位: w/m2, 0-100,0-100,0-100,0-100,°C, °C, m/s, m/s
  215. # 模型【输入输出】维度
  216. # 短期:输入 =(1,288,9),输出 =(1,288)
  217. # 中期:输出 =(1,960,9),输出 =(1,960)
  218. # 注意⚠️:特征顺序不要改变,保持上述描述顺序。
  219. # 根据 某个站点(args.projs),预报时间(args.init_time)取的气象数据
  220. init_time = args.init_time #YYYYMMDDHH
  221. ShortTerm_input = torch.randn(1, 288, 9)
  222. MediumTerm_input = torch.randn(1, 960, 9)
  223. #=====加载模型=======================================================================
  224. # 加载短期功率预测模型
  225. ShortTerm_Modelloader = ModelLoading(
  226. trial = 'None',
  227. projs = args.projs,
  228. model_name = args.model_name,
  229. release = args.release,
  230. input_seq = args.short_term_seq,
  231. saved_model_path = args.saved_model_root,
  232. predict_mode = True,
  233. capacity = args.capacity
  234. )
  235. # 加载中期功率预测模型
  236. MediumTerm_Modelloader = ModelLoading(
  237. trial = 'None',
  238. projs = args.projs,
  239. model_name = args.model_name,
  240. release = args.release,
  241. input_seq = args.medium_term_seq,
  242. saved_model_path = args.saved_model_root,
  243. predict_mode = True,
  244. capacity = args.capacity
  245. )
  246. #=====预测==========================================================================
  247. # 短期预测 -输出(288,)
  248. shortTerm_result = ShortTerm_Modelloader.predict(ShortTerm_input).detach().numpy().reshape(-1)
  249. ShortTerm_df = build_forecast_df(shortTerm_result, args.init_time)
  250. # 中期预测 - 输出(960,)
  251. MediumTerm_result = MediumTerm_Modelloader.predict(MediumTerm_input).detach().numpy().reshape(-1)
  252. MediumTerm_df = build_forecast_df(MediumTerm_result, args.init_time)
  253. #=====后处理-消除夜间非0值和可能存在的赋值===============================================
  254. ShortTerm_df = remove_night_power(ShortTerm_df, lat=args.lat, lon=args.lon)
  255. MediumTerm_df = remove_night_power(MediumTerm_df, lat=args.lat, lon=args.lon)
  256. print('测试成功🏅')