pain_yawslip.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. # -*- coding: utf-8 -*-
  2. """偏航滑移 —— 真实机群实测。
  3. 数据源: 50 台机组的滑移事件榜 (逐台次数 / 最大滑移角 / 逐月分布)。已脱敏: 台号重编, 不含场名与厂商。
  4. 下排三种信号为合成示意, 用于教"怎么分" —— 已机器自检: 滑移轨迹须过判据第 3、4 门。"""
  5. import os, json, numpy as np, matplotlib
  6. matplotlib.use("Agg"); import matplotlib.pyplot as plt
  7. from matplotlib import font_manager as fm
  8. for c in ["PingFang SC","Hiragino Sans GB","Songti SC","STHeiti"]:
  9. try:
  10. fm.findfont(fm.FontProperties(family=c), fallback_to_default=False)
  11. matplotlib.rcParams["font.sans-serif"]=[c,"DejaVu Sans"]; break
  12. except Exception: pass
  13. matplotlib.rcParams["axes.unicode_minus"]=False
  14. BG="#edf7f9"; INK="#133047"; INK2="#28536b"; MUT="#61788b"; LINE="#cbe2eb"
  15. SPEC="#0e8898"; MEAS="#2077a8"; WARN="#d48818"; BAD="#d94b43"; OK="#23875b"
  16. rng=np.random.default_rng(7)
  17. SRC="/Users/yuanying/wind-analytics/outputs/guojiadian_locked/偏航滑移健康榜.json"
  18. Y=json.load(open(SRC, encoding="utf-8"))
  19. cnt=np.array([t["全窗滑移"] for t in Y]); mx=np.array([t["最大°"] for t in Y])
  20. mo={}
  21. for t in Y:
  22. for k,v in (t.get("permonth") or {}).items(): mo[k]=mo.get(k,0)+v
  23. months=sorted(mo); vals=[mo[m] for m in months]
  24. order=np.argsort(-cnt)
  25. fig=plt.figure(figsize=(14.6,8.8), facecolor=BG)
  26. gs=fig.add_gridspec(2,3,left=.055,right=.985,top=.775,bottom=.115,wspace=.28,hspace=.60,
  27. height_ratios=[1,1.02])
  28. # ① 逐台滑移次数 (真实)
  29. ax=fig.add_subplot(gs[0,0]); ax.set_facecolor("#fff")
  30. col=[BAD if c>=10 else (WARN if c>0 else "#c9dbe4") for c in cnt[order]]
  31. ax.bar(range(len(cnt)),cnt[order],color=col,width=.86)
  32. ax.set_xlabel(f"{len(cnt)} 台机组(按次数排序,台号已重编)",color=MUT,fontsize=10)
  33. ax.set_ylabel("全窗滑移次数",color=MUT,fontsize=10.5)
  34. ax.set_title("① 一半机组一次没滑,问题集中在少数台",color=INK,fontsize=13,loc="left",pad=10)
  35. ax.annotate(f"{int(cnt.max())} 次",(0,cnt.max()),xytext=(10,-4),textcoords="offset points",
  36. color=BAD,fontsize=12,weight="bold")
  37. ax.text(.52,.72,f"零滑移 {int((cnt==0).sum())} 台\n有滑移 {int((cnt>0).sum())} 台\n合计 {int(cnt.sum())} 次",
  38. transform=ax.transAxes,fontsize=11.5,color=INK2,va="top")
  39. # ② 最大滑移角 (真实)
  40. ax2=fig.add_subplot(gs[0,1]); ax2.set_facecolor("#fff")
  41. nz=mx[mx>0]
  42. ax2.hist(nz,bins=np.arange(0,50,4),color=MEAS,alpha=.85,edgecolor="#fff")
  43. ax2.axvline(20,color=BAD,ls="--",lw=1.8)
  44. ax2.text(.97,.93,f"超 20° 的有 {int((mx>20).sum())} 台",transform=ax2.transAxes,ha="right",va="top",color=BAD,fontsize=11.5,weight="bold")
  45. ax2.set_xlabel("单次最大滑移角 (°)",color=MUT,fontsize=10.5)
  46. ax2.set_ylabel("台数",color=MUT,fontsize=10.5)
  47. ax2.set_title(f"② 最深一次滑了 {mx.max():.1f}°",color=INK,fontsize=13,loc="left",pad=10)
  48. # ③ 逐月 (真实) —— 有断点
  49. ax3=fig.add_subplot(gs[0,2]); ax3.set_facecolor("#fff")
  50. cols=[BAD if v>=40 else MEAS for v in vals]
  51. ax3.bar(range(len(months)),vals,color=cols,width=.72)
  52. ax3.set_xticks(range(len(months))); ax3.set_xticklabels([m[2:] for m in months],rotation=45,ha="right",fontsize=8.6)
  53. ax3.set_ylabel("当月滑移次数",color=MUT,fontsize=10.5)
  54. ax3.set_title("③ 不是均匀发生的,有明显断点",color=INK,fontsize=13,loc="left",pad=10)
  55. i0=months.index("2026-03") if "2026-03" in months else None
  56. if i0 is not None:
  57. ax3.annotate("此月 0 次",(i0,1),xytext=(-6,34),textcoords="offset points",
  58. arrowprops=dict(arrowstyle="->",color=INK2,lw=1.2),color=INK2,fontsize=10.5)
  59. ax3.text(.03,.93,"随后两月合计 %d 次" % (mo.get("2026-04",0)+mo.get("2026-05",0)),
  60. transform=ax3.transAxes,color=BAD,fontsize=11.5,weight="bold",va="top")
  61. # 下排: 三种信号怎么分 (合成, 教学)
  62. t=np.linspace(0,6,720)
  63. wd=8*np.sin(2*np.pi*t/9.5)+rng.normal(0,1.1,t.size).cumsum()*0.05
  64. pos_s=np.zeros_like(t); cur=0.
  65. for i in range(t.size):
  66. cur+=0.012*(wd[i]-cur); pos_s[i]=cur
  67. pos_s+=rng.normal(0,.05,t.size)
  68. pos_n=np.zeros_like(t); cmd_n=np.zeros_like(t); cur=0.
  69. for i in range(t.size):
  70. if abs(wd[i]-cur)>6.0: cur=wd[i]; cmd_n[i]=1
  71. pos_n[i]=cur
  72. pos_n+=rng.normal(0,.12,t.size)
  73. pos_e=wd*0.9+rng.normal(0,.15,t.size)
  74. for j in rng.choice(np.arange(60,t.size-60),5,replace=False): pos_e[j:]+=rng.choice([-14,-9,11,16])
  75. # 自检: 滑移须过门3(缓慢连续) 门4(与风载同侧)
  76. assert np.corrcoef(np.gradient(pos_s),np.gradient(wd))[0,1]>0, "滑移信号未过门4"
  77. assert np.abs(np.diff(pos_s)).max()<0.5, "滑移信号未过门3"
  78. for k,(name,colr,desc,pos,cmd) in enumerate([
  79. ("滑移",BAD,"无指令 · 缓慢连续 · 跟着风载方向",pos_s,np.zeros_like(t)),
  80. ("正常偏航",OK,"有指令 · 越死区才动 · 阶跃式",pos_n,cmd_n),
  81. ("编码器故障",WARN,"位置突跳 · 与风向无关 · 可正可负",pos_e,np.zeros_like(t))]):
  82. a=fig.add_subplot(gs[1,k]); a.set_facecolor("#fff")
  83. a.plot(t,wd,color=MEAS,lw=1.4,alpha=.6,label="风向")
  84. a.plot(t,pos,color=colr,lw=2.3,label="机舱位置")
  85. if cmd.max()>0:
  86. for x in t[cmd>0]: a.axvline(x,color=OK,lw=.9,alpha=.45)
  87. a.plot([],[],color=OK,lw=.9,label="偏航指令")
  88. a.set_title(f"④ {name}",color=colr,fontsize=13,loc="left",pad=26,weight="bold")
  89. a.text(.0,1.045,desc,transform=a.transAxes,fontsize=10.4,color=INK2,va="bottom")
  90. a.set_xlabel("时间 (小时)",color=MUT,fontsize=10)
  91. if k==0: a.set_ylabel("角度 (°)",color=MUT,fontsize=10)
  92. a.legend(loc="upper left",fontsize=9,frameon=False,labelcolor=INK2)
  93. for a in fig.axes:
  94. a.tick_params(colors=MUT,labelsize=9.2)
  95. for s in a.spines.values(): s.set_color(LINE)
  96. a.grid(color=LINE,lw=.7,alpha=.55)
  97. fig.text(.055,.945,"偏航滑移:制动压不住风,机舱被推着走",color=INK,fontsize=20,weight="bold")
  98. fig.text(.055,.900,f"上排是 {len(cnt)} 台机组的实测滑移事件。一半机组一次没滑,问题集中在少数台 —— "
  99. "这正是机群相对筛查能做的事。",color=INK2,fontsize=12.5)
  100. fig.text(.055,.862,"下排是三种在信号上长得很像的现象。判滑移必须四门同时成立:"
  101. "无偏航指令 · 风速够大 · 变化缓慢连续 · 方向与风载同侧。四门缺一就会误判。",color=MUT,fontsize=11)
  102. fig.text(.055,.022,"上排为实测事件统计(台号已重编,不含场名与厂商);下排为合成示意信号,"
  103. "用于教学「怎么分」,其滑移轨迹已机器自检通过判据第 3、4 门。",color=MUT,fontsize=9.3)
  104. out=os.path.join(os.path.dirname(__file__),"assets","pain_yawslip.png")
  105. fig.savefig(out,dpi=150,facecolor=BG); print("SAVED",out)
  106. print(f"真实: {len(cnt)} 台 | 滑移 {int(cnt.sum())} 次 | 零滑移 {int((cnt==0).sum())} 台 | 最大角 {mx.max():.1f}° | >20° {int((mx>20).sum())} 台")