pain_pitch_imbalance.py 5.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. # -*- coding: utf-8 -*-
  2. """变桨/叶轮角度不一致 -> 气动不平衡 -> 1P 激励 -> 载荷抬升。
  3. ① 机理: 三叶片推力随方位角, 可正演(相对量)。
  4. ② 实测: 一个真实机群的塔筒 1P 筛查结果 (机群相对, 去共模 z 分数)。台号重编, 不含场名与厂商。
  5. ③ 后果: 电量与寿命 —— 运行数据无载荷/应变通道, 本模型不自算, 全部引文献。"""
  6. import os, json, numpy as np, matplotlib
  7. matplotlib.use("Agg"); import matplotlib.pyplot as plt
  8. from matplotlib import font_manager as fm
  9. for c in ["PingFang SC","Hiragino Sans GB","Songti SC","STHeiti"]:
  10. try:
  11. fm.findfont(fm.FontProperties(family=c), fallback_to_default=False)
  12. matplotlib.rcParams["font.sans-serif"]=[c,"DejaVu Sans"]; break
  13. except Exception: pass
  14. matplotlib.rcParams["axes.unicode_minus"]=False
  15. BG="#edf7f9"; INK="#133047"; INK2="#28536b"; MUT="#61788b"; LINE="#cbe2eb"
  16. SPEC="#0e8898"; MEAS="#2077a8"; WARN="#d48818"; BAD="#d94b43"; OK="#23875b"
  17. H,R,ALPHA,K = 90.,60.,0.20,0.060
  18. psi=np.linspace(0,4*np.pi,900)
  19. def blade_moment(dt):
  20. My=np.zeros_like(psi)
  21. for i in range(3):
  22. a=psi+i*2*np.pi/3; z=H+R*np.cos(a)
  23. My+=(np.maximum(z,1.)/H)**(2*ALPHA)*(1.0-K*(dt if i==0 else 0.))*np.cos(a)
  24. return My,(np.abs(np.fft.rfft(My[:600]))[1])/300.
  25. M=json.load(open("/Users/yuanying/wind-analytics/outputs/blade_imbalance/mingyang_tower1p_screen.json",
  26. encoding="utf-8"))
  27. sc=np.array([t["score_dc"] for t in M]); tp=[t["type"] for t in M]
  28. o=np.argsort(-sc)
  29. fig=plt.figure(figsize=(14.6,5.9), facecolor=BG)
  30. gs=fig.add_gridspec(1,3,left=.055,right=.985,top=.665,bottom=.165,wspace=.30)
  31. ax=fig.add_subplot(gs[0]); ax.set_facecolor("#fff")
  32. for d,lab,col in [(0.,"三叶片一致",OK),(2.,"一片偏 2°",BAD)]:
  33. My,_=blade_moment(d); ax.plot(psi/(2*np.pi),My,color=col,lw=2.6,label=lab)
  34. ax.set_xlim(0,2); ax.set_xlabel("风轮转过的圈数",color=MUT,fontsize=10.5)
  35. ax.set_ylabel("轮毂弯矩 (相对)",color=MUT,fontsize=10.5)
  36. ax.set_title("① 机理:一片偏了,每转一圈晃一次",color=INK,fontsize=13,loc="left",pad=10)
  37. ax.legend(loc="lower center",ncol=2,fontsize=10.5,frameon=False,labelcolor=INK2)
  38. ax2=fig.add_subplot(gs[1]); ax2.set_facecolor("#fff")
  39. CMAP={"Y轴lean":BAD,"Z轴lean":WARN,"混合":"#9fc4d4"}
  40. ax2.bar(range(len(sc)),sc[o],color=[CMAP.get(tp[i],"#9fc4d4") for i in o],width=.84)
  41. ax2.axhline(3,color=BAD,ls="--",lw=1.6); ax2.axhline(2,color=WARN,ls=":",lw=1.5)
  42. ax2.text(len(sc)*.98,3.15,"z = 3",color=BAD,fontsize=10.5,ha="right")
  43. ax2.text(len(sc)*.98,2.12,"z = 2",color=WARN,fontsize=10.5,ha="right")
  44. ax2.set_xlabel(f"{len(sc)} 台机组(台号已重编)",color=MUT,fontsize=10)
  45. ax2.set_ylabel("塔筒 1P 去共模 z 分数",color=MUT,fontsize=10.5)
  46. ax2.set_title("② 实测:42 台里只挑出 2 台",color=INK,fontsize=13,loc="left",pad=10)
  47. hs=[plt.Rectangle((0,0),1,1,color=v) for v in CMAP.values()]
  48. ax2.legend(hs,[f"{k} {tp.count(k)} 台" for k in CMAP],fontsize=9.4,frameon=False,
  49. labelcolor=INK2,loc="upper right")
  50. ax2.text(.30,.60,f"z≥3: {int((sc>=3).sum())} 台\nz≥2: {int((sc>=2).sum())} 台\n机群中位 {np.median(sc):+.2f}",
  51. transform=ax2.transAxes,fontsize=11,color=INK2,va="top")
  52. ax3=fig.add_subplot(gs[2]); ax3.set_facecolor("#fff"); ax3.axis("off")
  53. ax3.set_title("③ 后果:电量与寿命双损",color=INK,fontsize=13,loc="left",pad=10)
  54. rows=[("年发电量损失","0.6 %","Δθ = 2° 时;最不利 1.2 %",BAD),
  55. ("剩余寿命损失","放大 2.9 – 3.2 倍","非线性放大;非旋转部件受影响最大",BAD),
  56. ("设计容差","± 0.3° / 极差 0.6°","GL 1989–2010 · DIBt 1993 · DS 472",OK),
  57. ("行业超标比例","35.3 %","1100+ 台激光实测筛 195 台,欧美 2013–2021",WARN)]
  58. for i,(k,v,d,col) in enumerate(rows):
  59. y=.90-i*.235
  60. ax3.add_patch(plt.Rectangle((0.01,y-.185),0.97,.215,transform=ax3.transAxes,
  61. facecolor="#f2fafc",edgecolor=LINE,lw=1))
  62. ax3.text(.05,y-.035,k,transform=ax3.transAxes,fontsize=11,color=INK2,va="top")
  63. ax3.text(.05,y-.078,v,transform=ax3.transAxes,fontsize=14.5,weight="bold",color=col,va="top")
  64. ax3.text(.05,y-.150,d,transform=ax3.transAxes,fontsize=9.0,color=MUT,va="top")
  65. ax3.text(.01,-.055,"③ 引自 Saathoff 等 (2021),Wind Energy Science 6:1079-1087。",
  66. transform=ax3.transAxes,fontsize=9.6,color=MUT)
  67. for a in (ax,ax2):
  68. a.tick_params(colors=MUT,labelsize=9.4)
  69. for s in a.spines.values(): s.set_color(LINE)
  70. a.grid(color=LINE,lw=.7,alpha=.55)
  71. fig.text(.055,.930,"变桨角度不一致:肉眼看不出,机器每转一圈晃一次",color=INK,fontsize=20,weight="bold")
  72. fig.text(.055,.882,"三个叶片桨距不一致,气动推力就不再三重对称。合力矩多出一个「每转一圈来一次」的分量,"
  73. "直接打在轮毂、主轴和塔架上。",color=INK2,fontsize=12.5)
  74. fig.text(.055,.843,"偏差两三度,站在塔下看不出来 —— 但它同时吃电量和寿命,寿命那头还是非线性放大的。",
  75. color=MUT,fontsize=11)
  76. fig.text(.055,.030,"① 为气动不平衡正演(切变 + 推力随桨距变化,相对量);② 为真实机群塔筒 1P 筛查实测"
  77. "(台号已重编,不含场名与厂商);③ 运行数据无载荷与应变通道,本模型不出绝对载荷与寿命,数字全部引文献。",
  78. color=MUT,fontsize=9.3)
  79. out=os.path.join(os.path.dirname(__file__),"assets","pain_pitch.png")
  80. fig.savefig(out,dpi=150,facecolor=BG); print("SAVED",out)
  81. print(f"真实: {len(sc)} 台 | z>=3: {int((sc>=3).sum())} | z>=2: {int((sc>=2).sum())} | 中位 {np.median(sc):+.2f} | 最大 {sc.max():.2f}")