wake_anim.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """动态尾流仿真: 风向按实测风玫瑰扫过, 看尾流在真实机位上横扫。
  2. 布局 = 真实山地风场 90 台机位(已脱敏刚性变换); 风向频率 = 该场 1,218,810 条实测 10min 记录。
  3. 引擎 = PyWake (DTU, MIT) · Bastankhah-Porte-Agel 2014。"""
  4. import os, csv, math, numpy as np, matplotlib
  5. matplotlib.use("Agg")
  6. import matplotlib.pyplot as plt
  7. from matplotlib import font_manager as fm
  8. from pyproj import Transformer
  9. from py_wake.site import UniformSite
  10. from py_wake.wind_turbines import WindTurbine
  11. from py_wake.wind_turbines.power_ct_functions import PowerCtTabular
  12. from py_wake.literature.gaussian_models import Bastankhah_PorteAgel_2014
  13. from py_wake.flow_map import XYGrid
  14. for cand in ["PingFang SC","Hiragino Sans GB","Songti SC","STHeiti"]:
  15. try:
  16. fm.findfont(fm.FontProperties(family=cand), fallback_to_default=False)
  17. matplotlib.rcParams["font.sans-serif"]=[cand,"DejaVu Sans"]; break
  18. except Exception: continue
  19. matplotlib.rcParams["axes.unicode_minus"]=False
  20. # ---- 浅蓝主题 (与观澜仿真台页面同一套 token) ----
  21. BG="#edf7f9"; PANEL="#ffffff"; INK="#133047"; INK2="#28536b"; MUT="#61788b"
  22. LINE="#cbe2eb"; ACC="#0e8898"; MEAS="#2077a8"; AMB="#d48818"; BAD="#d94b43"; GRN="#23875b"
  23. from matplotlib.colors import LinearSegmentedColormap
  24. # 自由来流 = 近乎页面底色, 尾流越深越蓝
  25. WAKE_CMAP = LinearSegmentedColormap.from_list("guanlan_wake",
  26. ["#0d4a63", "#2077a8", "#6cb6d4", "#b6e0ec", "#e8f6fa"])
  27. DEF_CMAP = LinearSegmentedColormap.from_list("guanlan_deficit",
  28. ["#e8f6fa", "#f0d9a8", "#d48818", "#d94b43", "#8c2420"])
  29. t=Transformer.from_crs("EPSG:4545","EPSG:4326",always_xy=True)
  30. lat=[];lon=[]
  31. for r in csv.DictReader(open("/Users/yuanying/wind-analytics/app_ETL/configs/contracts/mawang_machine_map.csv",encoding="utf-8-sig")):
  32. try: a=float(r["lat"]); o=float(r["lon"])
  33. except Exception: continue
  34. if 73<o<136 and 18<a<54: lat.append(a); lon.append(o)
  35. elif 1e5<o<1e6 and 1e6<a<6e6:
  36. lo,la=t.transform(o,a); lat.append(la); lon.append(lo)
  37. lat=np.array(lat); lon=np.array(lon)
  38. R=6371000.
  39. x=R*np.radians(lon-lon.mean())*math.cos(math.radians(lat.mean())); y=R*np.radians(lat-lat.mean())
  40. TH=math.radians(20.0) # 脱敏旋转; 风向同步 **减去** 该角 (已验逐台不变)
  41. xr=x*math.cos(TH)-y*math.sin(TH); yr=x*math.sin(TH)+y*math.cos(TH)
  42. xr-=xr.min(); yr-=yr.min()
  43. ROT=math.degrees(TH)
  44. u=np.arange(0,26.); p=np.clip(((u-3)/8)**3,0,1)*2e6; p[u<3]=0; p[u>25]=0
  45. ct=np.where((u>=3)&(u<=25),np.clip(0.8*np.minimum(1,(11/np.maximum(u,3))**2),.05,.8),0)
  46. wt=WindTurbine(name="w",diameter=118.,hub_height=80.,powerCtFunction=PowerCtTabular(u,p,'w',ct))
  47. wfm=Bastankhah_PorteAgel_2014(UniformSite(p_wd=[1],ti=.11,ws=8.),wt,k=0.0324555)
  48. pad=1400.
  49. gx=np.linspace(xr.min()-pad, xr.max()+pad, 460)
  50. gy=np.linspace(yr.min()-pad, yr.max()+pad, 210)
  51. grid=XYGrid(x=gx, y=gy)
  52. ROSE=[0.6,2.5,2.7,4.0,14.2,33.5,26.6,8.1,2.7,2.2,2.4,0.5] # 实测, 30° 一档, 从 0° 起
  53. def rose_pct(wd_true):
  54. return ROSE[int((wd_true%360)//30)]
  55. WDS=list(range(90, 271, 5)) # 扫过实测主风区 (90-270° 覆盖约 92% 的时间)
  56. frames=os.path.join(os.path.dirname(__file__),"frames"); os.makedirs(frames, exist_ok=True)
  57. for f in os.listdir(frames): os.remove(os.path.join(frames,f))
  58. for k, wd_true in enumerate(WDS):
  59. WD=(wd_true-ROT)%360
  60. sim=wfm(xr,yr,wd=WD,ws=8.)
  61. fmap=sim.flow_map(grid=grid, wd=WD, ws=8.)
  62. Z=fmap.WS_eff.squeeze().values
  63. assert Z.shape==(len(gy),len(gx)), f"方向变了 {Z.shape}"
  64. dfc=(8.0-np.asarray(sim.WS_eff.squeeze().values,dtype=float))/8.0*100.
  65. # 不报 dfc.max(): 高斯尾流多重叠加会把有效风速压到近零(模型饱和), 真实尾流恢复更快,
  66. # 该极值不可作对外数字。改报稳健量: 受影响台数 + 平均亏损。
  67. n_hit = int((dfc > 10).sum()); dfc_mean = float(dfc.mean())
  68. fig=plt.figure(figsize=(14.4,7.2), facecolor=BG)
  69. ax=fig.add_axes([.052,.10,.70,.70]); ax.set_facecolor(BG)
  70. ax.contourf(gx,gy,Z,levels=np.linspace(5.6,8.15,48),cmap=WAKE_CMAP,extend="both")
  71. ax.scatter(xr,yr,s=30,c=dfc,cmap=DEF_CMAP,vmin=0,vmax=60,edgecolors="#ffffff",linewidths=.6,zorder=6)
  72. fu=-math.sin(math.radians(WD)); fv=-math.cos(math.radians(WD)); L=.09
  73. ax.annotate("",xy=(.06+fu*L,.12+fv*L),xytext=(.06,.12),xycoords="axes fraction",
  74. arrowprops=dict(arrowstyle="-|>",color=ACC,lw=2.4))
  75. ax.set_aspect("equal"); ax.set_xlim(gx[0],gx[-1]); ax.set_ylim(gy[0],gy[-1])
  76. ax.tick_params(colors=MUT,labelsize=8)
  77. for s in ax.spines.values(): s.set_color(LINE)
  78. ax.set_xlabel("场内相对坐标 (m) — 已脱敏",color=MUT,fontsize=9)
  79. axr=fig.add_axes([.782,.10,.175,.40],projection="polar"); axr.set_facecolor(BG)
  80. th=np.radians(np.arange(0,360,30)+15)
  81. axr.bar(th,ROSE,width=np.radians(29),color=ACC,alpha=.55,edgecolor=LINE)
  82. axr.bar(np.radians((wd_true//30)*30+15),ROSE[int((wd_true%360)//30)],width=np.radians(29),
  83. color=AMB,edgecolor="#8a5510")
  84. axr.plot([np.radians(wd_true)]*2,[0,36],color=INK,lw=2.0)
  85. axr.set_theta_zero_location("N"); axr.set_theta_direction(-1)
  86. axr.set_yticklabels([]); axr.tick_params(colors=MUT,labelsize=7)
  87. axr.grid(color=LINE,lw=.5); axr.spines["polar"].set_color(LINE)
  88. axr.set_title("实测风向分布",color=INK2,fontsize=9.5,pad=10)
  89. fig.text(.052,.945,"风向扫过时,尾流在整个场里横扫",color=INK,fontsize=19,weight="bold")
  90. fig.text(.052,.900,"90 台真实机位(已脱敏)· 风向频率来自 1,218,810 条实测 10 分钟记录 · 引擎 PyWake",
  91. color=MUT,fontsize=10)
  92. fig.text(.775,.745,"来流方向",color=INK2,fontsize=13)
  93. fig.text(.775,.665,f"{wd_true:d}°",color=INK,fontsize=34,weight="bold") # 数字用默认字体, 不指定 monospace
  94. fig.text(.775,.615,f"该风向占全年 {rose_pct(wd_true):.1f}%",color=AMB,fontsize=12.5)
  95. fig.text(.775,.585,f"受尾流影响 {n_hit} 台 · 平均亏损 {dfc_mean:.1f}%",color=INK2,fontsize=12.5)
  96. fig.savefig(os.path.join(frames,f"f{k:03d}.png"),dpi=110,facecolor=BG); plt.close(fig)
  97. if k%8==0: print("frame",k,"/",len(WDS),"wd",wd_true)
  98. print("FRAMES DONE", len(WDS))