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