build_collapse_3d.py 10 KB

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