| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- # -*- coding: utf-8 -*-
- """生成「运行形态塌陷 · 动态三维」交互页。
- 数据源: 如东 obs_sprt / mset_sprt 的 morphology_health_margin_grid.csv (各 1080 行 = 4 台 x 18 月 x 15 风速档)。
- 已脱敏: 台号重编为 机组 A-D, 不含场名与厂商。
- 边界(引自 method_metadata.json, 原文照搬, 不得删):
- - 本通道只用运行形态, 不含轴承/齿轮温度与振动特征
- - 1 分钟源数据止于 2026-06-30
- - 合格的 SPRT 候选是**审计条目, 不是部件故障结论**
- """
- import csv, json, pathlib, collections
- SRC = pathlib.Path("/Users/yuanying/wind-analytics/outputs/rudong/integrated_diagnostic_report")
- SIM = pathlib.Path("/Users/yuanying/guanlan-rudong-v2/outputs/rudong/windscada/_demo/sim")
- def load(algo):
- rows = list(csv.DictReader(open(SRC/algo/"morphology_health_margin_grid.csv", encoding="utf-8-sig")))
- meta = json.load(open(SRC/algo/"method_metadata.json", encoding="utf-8"))
- tids = sorted({r["tid"] for r in rows})
- months = sorted({r["month"] for r in rows})
- bins = sorted({int(float(r["wind_bin"])) for r in rows})
- alias = {t: f"机组 {chr(65+i)}" for i, t in enumerate(tids)} # 脱敏: 台号重编
- grid = {}
- for t in tids:
- z = [[None]*len(bins) for _ in months]
- for r in rows:
- if r["tid"] != t: continue
- v = r["health_margin"]
- if v in ("", "nan"): continue
- z[months.index(r["month"])][bins.index(int(float(r["wind_bin"])))] = round(float(v), 4)
- grid[alias[t]] = z
- return {"turbines": [alias[t] for t in tids], "months": months, "bins": bins,
- "grid": grid, "thr": next((meta[k] for k in ("obs_threshold_95","mset_threshold_95","threshold_95") if k in meta), None),
- "full_mset": not meta.get("not_true_mset", False),
- "limits": meta.get("limitations", []), "nfeat": len(meta.get("features", []))}
- DATA = {"简化记忆矩阵法": load("obs_sprt"), "完整相似算子法": load("mset_sprt")}
- J = json.dumps(DATA, ensure_ascii=False)
- HTML = f'''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
- <title>运行形态塌陷 · 动态三维</title>
- <script src="./plotly.min.js"></script>
- <style>
- :root{{--bg:#edf7f9;--panel:#fff;--ink:#133047;--ink2:#28536b;--ink3:#61788b;--line:#cbe2eb;
- --spec:#0e8898;--specf:#dff4f4;--meas:#2077a8;--warn:#d48818;--bad:#d94b43;
- --sans:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",ui-sans-serif,system-ui,sans-serif;--mono:"SF Mono",Menlo,monospace}}
- *{{box-sizing:border-box;margin:0}}body{{background:var(--bg);color:var(--ink);font:14.5px/1.7 var(--sans);-webkit-font-smoothing:antialiased}}
- .wrap{{max-width:1400px;margin:0 auto;padding:0 30px}}
- .gl-top{{display:flex;align-items:center;gap:14px;padding:11px 30px;background:linear-gradient(110deg,#10384d,#0e6470);color:#fff}}
- .gl-logo{{width:30px;height:30px;border-radius:8px;background:rgba(255,255,255,.16);display:grid;place-items:center;font-weight:700}}
- .gl-top b{{font-size:14.5px}} .gl-top small{{display:block;font:11px var(--mono);opacity:.75;letter-spacing:.1em}}
- .gl-back{{margin-left:auto;color:#e8fbff;text-decoration:none;border:1px solid #a7dce3;border-radius:999px;padding:6px 14px;font-size:12.5px}}
- section{{padding:26px 0}}
- .kick{{font:12px var(--mono);letter-spacing:.16em;color:var(--spec);text-transform:uppercase}}
- h1{{font-size:31px;line-height:1.2;font-weight:600;margin:12px 0 10px}}
- .lead{{color:var(--ink2);font-size:15.5px;max-width:920px;text-wrap:pretty}}
- .ctl{{display:flex;gap:22px;flex-wrap:wrap;align-items:center;background:#f7fcfd;border:1px solid var(--line);
- border-radius:12px;padding:14px 20px;margin:18px 0}}
- .ctl label{{font-size:13px;color:var(--ink2);margin-right:8px}}
- .ctl select,.ctl button{{font:13.5px var(--sans);padding:7px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer}}
- .ctl button.on{{background:var(--spec);color:#fff;border-color:var(--spec)}}
- #plot{{width:100%;height:660px;background:#fff;border:1px solid var(--line);border-radius:12px}}
- .note{{border-left:3px solid var(--spec);background:var(--specf);border-radius:0 10px 10px 0;padding:14px 18px;color:var(--ink2);font-size:13.8px;margin-top:16px}}
- .warnbox{{border-left:3px solid var(--bad);background:#fdf0ef;border-radius:0 10px 10px 0;padding:14px 18px;color:var(--ink2);font-size:13.8px;margin-top:14px}}
- .foot{{color:var(--ink3);font:11.5px var(--mono);padding:18px 0 34px}}
- </style></head><body>
- <div class="gl-top"><div class="gl-logo">观</div>
- <div><b>运行形态塌陷 · 动态三维</b><small>MORPHOLOGY COLLAPSE 3D</small></div>
- <a class="gl-back" href="./仿真集成台.html">← 返回仿真集成台</a></div>
- <div class="wrap"><section>
- <div class="kick">实测 · 可旋转可缩放</div>
- <h1>健康曲面上戳下去的那几口井</h1>
- <p class="lead">横轴风速档、纵轴月份、高度是<b>健康裕度</b>。绿色平面是正常,
- 裕度掉到零平面以下就是<b>形态塌陷</b> —— 在图上表现为从曲面往下扎的一口井。
- <b>拖动可旋转、滚轮可缩放、悬停看具体数值。</b></p>
- <div class="ctl">
- <div><label>算法</label><select id="algo"></select></div>
- <div><label>机组</label><select id="tid"></select></div>
- <div><label>视图</label>
- <button class="on" data-v="surface">曲面</button>
- <button data-v="all">四台并排</button>
- </div>
- <div style="margin-left:auto;color:var(--ink3);font-size:12.5px" id="stat"></div>
- </div>
- <div id="plot"></div>
- <div class="note"><b>怎么读:</b>零平面以上=该风速档该月的运行形态与参照机群一致;
- 往下扎的井=形态偏离,越深偏离越大。两种算法可切换对照 ——
- <b>同一口井两种算法都出现,才值得立案</b>;只有一种算法出现的,先当候选。<br><br><b>⚠ 深度数值不可跨算法比。</b>两法的诊断指标量纲差很远(阈值一个 0.754、一个 0.034),同一台机器在完整相似算子法下的井会深十几倍甚至上百倍 —— 那是量纲问题,不是它更坏。<b>可比的只有位置模式:哪些风速档、哪些月份塌了。</b>两法都在同一格出现井,才值得立案。</div>
- <div class="warnbox"><b>边界(引自方法元数据,原文照搬):</b><ul id="lims" style="margin:8px 0 0 20px"></ul></div>
- </section>
- <div class="foot">观澜 · 运行形态塌陷动态三维 · 数据已脱敏(台号重编,不含场名与厂商)· 离线单文件(图形库本地)</div>
- </div>
- <script>
- const D = {J};
- const algoSel=document.getElementById('algo'), tidSel=document.getElementById('tid');
- Object.keys(D).forEach(k=>algoSel.add(new Option(k,k)));
- let VIEW='surface';
- function cur(){{ return D[algoSel.value]; }}
- function fillTid(){{ const c=cur(); tidSel.innerHTML=''; c.turbines.forEach(t=>tidSel.add(new Option(t,t))); }}
- const CS=[[0,'#7f1d1d'],[0.35,'#d94b43'],[0.55,'#f0c674'],[0.68,'#e8f6fa'],[0.8,'#9ed7c4'],[1,'#0e8898']];
- let CMIN=-1.2;
- function surf(c,t,scene){{ return {{type:'surface', x:c.bins, y:c.months, z:c.grid[t],
- colorscale:CS, cmin:CMIN, cmax:0.9, showscale:scene==='scene',
- colorbar:{{title:{{text:'健康裕度',side:'right'}},thickness:14,len:.7}},
- contours:{{z:{{show:true,usecolormap:true,project:{{z:true}}}}}},
- hovertemplate:'风速档 %{{x}} m/s<br>月份 %{{y}}<br>健康裕度 %{{z:.3f}}<extra></extra>',
- scene:scene}}; }}
- function draw(){{
- const c=cur();
- {{const a=Object.values(c.grid).flat(2).filter(v=>v!==null).sort((x,y)=>x-y);
- CMIN=Math.max(a[Math.floor(a.length*0.05)]||-1.2, -6);}}
- const L={{paper_bgcolor:'#fff',plot_bgcolor:'#fff',
- font:{{family:'PingFang SC,Hiragino Sans GB,sans-serif',color:'#28536b',size:12}},
- margin:{{l:0,r:0,t:26,b:0}}}};
- // z 轴按本算法数据自适应: 取全体 5% 分位为下界(极端离群不主导视野), 并在读数里报真实最深值。
- // 固定 range 会让另一算法的井全部落在轴外看不见(曾错)。
- const allv=Object.values(c.grid).flat(2).filter(v=>v!==null).sort((a,b)=>a-b);
- const lo=Math.max(allv[Math.floor(allv.length*0.05)]||-2, -6);
- const CLIP=Math.min(lo,-0.6);
- const ax=n=>({{xaxis:{{title:'风速档 m/s'}},yaxis:{{title:'月份'}},
- zaxis:{{title:'健康裕度(已按分位截断显示)',range:[CLIP,1.0]}},
- camera:{{eye:{{x:1.7,y:-1.5,z:0.85}}}},aspectratio:{{x:1.15,y:1.5,z:.75}}}});
- if(VIEW==='surface'){{
- Plotly.react('plot',[surf(c,tidSel.value,'scene')],
- Object.assign({{}},L,{{scene:ax(),title:{{text:tidSel.value,x:.02,font:{{size:15,color:'#133047'}}}}}}),
- {{displaylogo:false,responsive:true}});
- }} else {{
- const traces=[], lay=Object.assign({{}},L,{{grid:{{rows:2,columns:2,pattern:'independent'}},showlegend:false}});
- c.turbines.forEach((t,i)=>{{ const s='scene'+(i?i+1:''); traces.push(surf(c,t,s)); lay[s]=ax(); }});
- lay.annotations=c.turbines.map((t,i)=>({{text:t,showarrow:false,x:(i%2)*.52+.03,y:i<2?1.0:.47,
- xref:'paper',yref:'paper',font:{{size:14,color:'#133047'}}}}));
- Plotly.react('plot',traces,lay,{{displaylogo:false,responsive:true}});
- }}
- const g=c.grid[tidSel.value].flat().filter(v=>v!==null);
- const below=g.filter(v=>v<0).length;
- const mn=Math.min(...g);
- document.getElementById('stat').textContent =
- `${{tidSel.value}} · 真实最深 ${{mn.toFixed(2)}}${{mn<CLIP?'(图上已截断至 '+CLIP.toFixed(1)+')':''}} · 零平面以下 ${{below}}/${{g.length}} 格`;
- const ul=document.getElementById('lims'); ul.innerHTML='';
- c.limits.forEach(x=>{{ const li=document.createElement('li'); li.textContent=x; ul.appendChild(li); }});
- }}
- document.querySelectorAll('.ctl button').forEach(b=>b.onclick=()=>{{
- document.querySelectorAll('.ctl button').forEach(x=>x.classList.remove('on'));
- b.classList.add('on'); VIEW=b.dataset.v; draw(); }});
- algoSel.onchange=()=>{{fillTid();draw();}}; tidSel.onchange=draw;
- fillTid(); draw();
- </script></body></html>'''
- p = SIM/"运行形态塌陷_动态三维.html"
- p.write_text(HTML, encoding="utf-8")
- print(f"written {p.name} {len(HTML)//1024} KB")
- for k,v in DATA.items():
- print(f" {k}: {len(v['turbines'])} 台 x {len(v['months'])} 月 x {len(v['bins'])} 档 | 阈值 {v['thr']:.4f} | 完整MSET={v['full_mset']} | 限制 {len(v['limits'])} 条")
- leaks={k:HTML.count(k) for k in ["如东","33A","17B","23C","38B","rudong"] if HTML.count(k)}
- print(" 标识残留:", leaks or "0 ✓")
|