<!doctype html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>如东 SWT-4.0-130 控制律仪表台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans+Condensed:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500&display=swap">
<style>
:root{
--bg:#eef2f3; --panel:#ffffff; --panel2:#f6f9fa; --line:#c9d6d9; --line2:#e2ebed;
--ink:#12262b; --ink2:#476a71; --ink3:#7b979d;
--spec:#0d7d92; --specf:#0d7d9218; --meas:#b4650a; --measf:#b4650a1c;
--ok:#25774a; --bad:#b23c2e; --warn:#8a6d1f; --nil:#6d7f84;
--shadow:0 1px 2px #12262b0f,0 6px 20px #12262b0a;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg:#0a1417; --panel:#101f23; --panel2:#0d1a1e; --line:#22383e; --line2:#1a2c31;
--ink:#dbe8ea; --ink2:#8daab1; --ink3:#5f7b82;
--spec:#48cfe4; --specf:#48cfe422; --meas:#f2a54a; --measf:#f2a54a20;
--ok:#66c98c; --bad:#ef8375; --warn:#d9b45c; --nil:#7d949a;
--shadow:0 1px 2px #0006,0 8px 26px #0004;
}}
:root[data-theme="dark"]{
--bg:#0a1417; --panel:#101f23; --panel2:#0d1a1e; --line:#22383e; --line2:#1a2c31;
--ink:#dbe8ea; --ink2:#8daab1; --ink3:#5f7b82;
--spec:#48cfe4; --specf:#48cfe422; --meas:#f2a54a; --measf:#f2a54a20;
--ok:#66c98c; --bad:#ef8375; --warn:#d9b45c; --nil:#7d949a;
--shadow:0 1px 2px #0006,0 8px 26px #0004;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:400 14px/1.55 "IBM Plex Sans","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
-webkit-font-smoothing:antialiased}
.wrap{max-width:1240px;margin:0 auto;padding:28px 20px 60px}
h1{font:700 27px/1.2 "IBM Plex Sans Condensed","PingFang SC",sans-serif;margin:0 0 6px;letter-spacing:.01em;text-wrap:balance}
.sub{color:var(--ink2);margin:0 0 4px;max-width:66ch}
.prov{font:400 12px/1.6 "IBM Plex Mono",monospace;color:var(--ink3);margin:10px 0 0}
.chips{display:flex;flex-wrap:wrap;gap:7px;margin:20px 0 6px}
.chip{display:flex;align-items:center;gap:7px;padding:5px 11px 5px 9px;border:1px solid var(--line);
border-radius:100px;background:var(--panel);font:500 12.5px/1 "IBM Plex Sans Condensed","PingFang SC",sans-serif}
.chip b{font:600 12.5px/1 "IBM Plex Mono",monospace}
.chip .sy{font:700 9.5px/1 "IBM Plex Mono",monospace;color:var(--ink3);border:1px solid var(--line);padding:2px 4px;border-radius:3px}
.dot{width:7px;height:7px;border-radius:50%;flex:none}
.c-ok .dot{background:var(--ok)}.c-ok b{color:var(--ok)}
.c-bad .dot{background:var(--bad)}.c-bad b{color:var(--bad)}
.c-nil .dot{background:var(--nil)}.c-nil b{color:var(--nil)}
.c-warn .dot{background:var(--warn)}.c-warn b{color:var(--warn)}
.ctl{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:22px 0 26px;padding:14px 16px;
background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)}
.ctl label{display:flex;align-items:center;gap:9px;font:500 12.5px/1 "IBM Plex Sans Condensed","PingFang SC",sans-serif;color:var(--ink2)}
.ctl input[type=range]{width:150px;accent-color:var(--spec)}
.ctl output{font:600 13px/1 "IBM Plex Mono",monospace;color:var(--ink);min-width:56px}
button{font:600 12.5px/1 "IBM Plex Sans Condensed","PingFang SC",sans-serif;padding:7px 14px;border-radius:7px;
border:1px solid var(--line);background:var(--panel2);color:var(--ink);cursor:pointer}
button:hover{border-color:var(--spec);color:var(--spec)}
button:focus-visible{outline:2px solid var(--spec);outline-offset:2px}
.sys{margin:30px 0 0}
.sysh{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;padding:0 2px 10px;border-bottom:2px solid var(--ink);margin-bottom:16px}
.sysh .n{font:700 12px/1 "IBM Plex Mono",monospace;color:var(--panel);background:var(--ink);padding:5px 8px;border-radius:4px;letter-spacing:.06em}
.sysh h2{font:700 19px/1.2 "IBM Plex Sans Condensed","PingFang SC",sans-serif;margin:0}
.sysh .ch{font:500 11.5px/1 "IBM Plex Mono",monospace;color:var(--ink3)}
.sysh .sm{margin-left:auto;font:500 12px/1 "IBM Plex Sans Condensed","PingFang SC",sans-serif;color:var(--ink2)}
.sysh .sm em{font-style:normal;font-weight:600;font-family:"IBM Plex Mono",monospace}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
.panel.wide{grid-column:1/-1}
.ph{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:14px 16px 11px;border-bottom:1px solid var(--line2)}
.ph h2{font:600 16px/1.25 "IBM Plex Sans Condensed","PingFang SC",sans-serif;margin:0}
.ph .code{font:500 11.5px/1 "IBM Plex Mono",monospace;color:var(--ink3);letter-spacing:.02em}
.ph .vd{margin-left:auto;font:600 11.5px/1 "IBM Plex Mono",monospace;padding:4px 9px;border-radius:5px}
.vd-ok{color:var(--ok);background:#25774a1a}.vd-bad{color:var(--bad);background:#b23c2e1a}
.vd-nil{color:var(--nil);background:#6d7f841a}.vd-warn{color:var(--warn);background:#8a6d1f1a}
.pb{display:grid;grid-template-columns:auto minmax(0,1fr);gap:16px;padding:16px}
.pb.stack{grid-template-columns:minmax(0,1fr)}
.pb.stack .rd{display:grid;grid-template-columns:repeat(auto-fit,minmax(196px,1fr));gap:7px 22px;align-content:start}
.pb.stack .rd hr.sep{grid-column:1/-1;margin:2px 0}
.pb.stack canvas.sim{width:100%!important;max-width:100%}
canvas{display:block;max-width:100%}
.sim{border-radius:8px;background:var(--panel2)}
.rd{display:flex;flex-direction:column;gap:9px;min-width:0}
.row{display:flex;align-items:baseline;gap:8px;font:400 12.5px/1.3 "IBM Plex Sans","PingFang SC",sans-serif;color:var(--ink2)}
.row .k{flex:1;min-width:0}
.row .v{font:500 13px/1.2 "IBM Plex Mono",monospace;color:var(--ink);font-variant-numeric:tabular-nums;white-space:nowrap}
.row.s .v{color:var(--spec)}.row.m .v{color:var(--meas)}
.tag{font:600 10px/1 "IBM Plex Mono",monospace;padding:3px 5px;border-radius:4px;letter-spacing:.03em}
.t-s{color:var(--spec);background:var(--specf)}.t-m{color:var(--meas);background:var(--measf)}
.note{font:400 12.5px/1.6 "IBM Plex Sans","PingFang SC",sans-serif;color:var(--ink2);
padding:12px 16px;background:var(--panel2);border-top:1px solid var(--line2)}
.note b{color:var(--ink);font-weight:600}
.note code{font:500 12px/1 "IBM Plex Mono",monospace;color:var(--meas)}
hr.sep{border:0;border-top:1px solid var(--line2);margin:4px 0}
@media (max-width:900px){.grid{grid-template-columns:minmax(0,1fr)}.pb{grid-template-columns:minmax(0,1fr)}
.sim{margin:0 auto}}
</style>
<style>html{color-scheme:light dark}body{margin:0}img{max-width:100%}[hidden]{display:none!important}
.sys{margin-top:8px}.wrap{padding-top:24px}</style>
<style id="guanlan-control-layout">
:root[data-theme=light]{--bg:#eef6f8;--panel:#fff;--panel2:#f3f8fa;--ink:#163546;--ink2:#526f80;--ink3:#607f8e;--line:#d3e3e9;--line2:#e4eef2;--spec:#087f90;--shadow:0 8px 28px #16354608;color-scheme:light}
.wrap{max-width:1440px;padding:24px 28px 64px}.control-hero{position:relative;overflow:hidden;padding:36px;border-radius:22px;background:linear-gradient(115deg,#0b293a,#125264);color:white;margin-bottom:24px}.control-hero:after{content:"";position:absolute;right:0;top:0;width:36%;height:100%;opacity:.22;background:repeating-linear-gradient(0deg,transparent 0 31px,#73cbd6 32px),repeating-linear-gradient(90deg,transparent 0 31px,#73cbd6 32px);pointer-events:none}.control-hero h1{font-size:clamp(25px,3vw,37px);letter-spacing:.02em;position:relative;z-index:1}.control-hero .sub{color:#c5e2e9;line-height:1.85;margin-top:18px;position:relative;z-index:1}.control-hero .prov{color:#abd0d9;max-width:100ch;position:relative;z-index:1;overflow-wrap:anywhere}.control-eyebrow{font:600 12px/1.5 monospace;letter-spacing:.16em;color:#75d6ce;margin-bottom:15px}.control-art{position:absolute;right:24px;top:20px;width:28%;height:100px;opacity:.2;pointer-events:none}
.chips{gap:10px}.chip{min-height:36px;border-radius:8px}.ctl{padding:20px;gap:18px;border-radius:16px;background:var(--panel);margin:22px 0 38px}.ctl label{flex:1;min-width:240px;line-height:1.5}.ctl input[type=range]{min-width:60px;flex:1;height:44px}.ctl button{min-height:44px;padding:12px 20px}#pz{background:#087e8c;color:white;border-color:#087e8c}.sysh{border-bottom:1px solid var(--line);padding:0 0 16px;gap:12px}.sysh .n{background:#087e8c;color:white;padding:10px;border-radius:9px}.sysh h2{font-size:23px}.sysh .ch,.sysh .sm{line-height:1.7}.sys{margin-top:38px}.panel{border-radius:17px}.ph{padding:19px 20px;background:var(--panel2)}.ph h2{font-size:18px}.pb{padding:20px;gap:22px}.note{padding:16px 20px;line-height:1.8}.row{line-height:1.6}.grid{gap:22px}.control-bottom{display:block;margin-top:28px;color:var(--spec)}
@media(max-width:600px){.wrap{padding:12px 12px 40px}.control-hero{padding:25px 20px;border-radius:16px}.control-art{display:none}.control-hero:after{width:100%;opacity:.07}.ctl{padding:14px;gap:12px}.ctl label{flex-basis:100%;min-width:0}.ctl button{flex:1}.sysh .sm{margin-left:0}.pb{padding:14px}.row{flex-wrap:wrap}.row .v{white-space:normal;overflow-wrap:anywhere}.prov{overflow-wrap:anywhere}.ph{padding:16px}.pb.stack .rd{grid-template-columns:minmax(0,1fr)}}
</style><style>#th{display:none}.subsystem-back{display:inline-block;margin-bottom:18px;padding:10px 16px;border:1px solid #cfe1e8;border-radius:9px;color:#087e8c;text-decoration:none;background:white;min-height:44px}</style></head>
<body>
<div class="wrap">
<a class="subsystem-back" href="#" onclick="if(parent!==window){parent.document.getElementById('ovx').click();}return false">← 返回仿真目录</a><header class="control-hero"><div class="control-eyebrow">SWT-4.0-130 · 发电机与热管理</div><h1 id="h1">如东 SWT-4.0-130 控制律仪表台</h1>
<p class="sub">左边是参数表说机器<em>应该</em>怎么动,右边是 38 台机器 553 天<em>实际</em>怎么动。按系统分区:偏航、传动链与功率、变桨与轮毂液压。同一根风驱动全部三个系统。各面板对照控制律演示与运行数据统计。</p>
<p class="prov">参数 = WTG05 整机回读 1706 项(实机回读级) · 数据 = 10min 原生层 297 万行 + 1min 转发层 2984 万行 · 2025-01-01 → 2026-07-07</p>
</header><div class="chips" id="chips"></div>
<div class="ctl">
<label>风速 <input type="range" id="ws" min="0" max="25" step="0.1" value="7.5"><output id="wso">7.5 m/s</output></label>
<label data-sys="yaw">风向扰动 <input type="range" id="tb" min="0" max="3" step="0.05" value="1"><output id="tbo">1.0×</output></label>
<label>时间 <input type="range" id="sp" min="1" max="60" step="1" value="12"><output id="spo">12×</output></label>
<label data-sys="gen">环温 <input type="range" id="amb" min="-5" max="40" step="1" value="18"><output id="ambo">18 °C</output></label>
<button id="pz">暂停</button>
<button id="th">切换明暗</button>
</div>
<div id="stack"></div>
<p class="prov" style="margin-top:26px">判据与证伪条件全量见 <code>outputs/rudong/yaw_verify/findings.json</code>;逐台明细 <code>deadband_per_turbine.csv</code>。仿真为参数表控制律的正向实现,非 OEM 源码;实测曲线为运行数据统计,两者独立。</p>
</div>
<script>
const SYSFILTER="gen";
const SF=(typeof SYSFILTER!=="undefined")?SYSFILTER:null;
const TH={"rise_exponent":0.39,"rise_k":57.3,"tau_min":139.1,"brg_G":{"dT_full":7.0,"p99":62.1},"brg_R":{"dT_full":26.0,"p99":74.0},"stator_p99":108.3,"stator_max":152.0,"above":{"80":22.659,"130.2":0.0015,"100":3.329,"155":0.0},"steady_exponent":0.49,"A_noload":24.8,"B_copper":42.2,"dT_full":67.0,"steady_n":843900,"curve":{"P":[0.05,0.15,0.25,0.45,0.55,0.75,0.85,0.95],"dT":[14.0,23.2,29.7,43.0,42.4,51.6,52.4,59.5]},"Thist":{"c":[1.2,3.8,6.2,8.8,11.2,13.8,16.2,18.8,21.2,23.8,26.2,28.8,31.2,33.8,36.2,38.8,41.2,43.8,46.2,48.8,51.2,53.8,56.2,58.8,61.2,63.8,66.2,68.8,71.2,73.8,76.2,78.8,81.2,83.8,86.2,88.8,91.2,93.8,96.2,98.8,101.2,103.8,106.2,108.8,111.2,113.8,116.2,118.8,121.2,123.8,126.2,128.8,131.2,133.8,136.2,138.8,141.2,143.8,146.2,148.8,151.2,153.8,156.2],"p":[0.0115,0.0087,0.0306,0.0177,0.0837,0.1294,0.1991,0.2042,0.3604,0.4694,0.6369,0.8029,1.2158,1.3784,2.5527,2.8953,4.3407,4.4307,5.1335,4.8886,4.9664,4.679,4.5515,4.2455,4.0388,3.7808,3.6035,3.5258,3.5037,3.53,3.5462,3.5796,3.4818,3.3921,3.0719,2.6505,2.1075,1.8735,1.4781,1.2734,0.8725,0.658,0.5849,0.5294,0.4007,0.2202,0.0404,0.0213,0.0005,0.0004,0.0003,0.0005,0.0003,0.0002,0.0003,0.0002,0.0001,0.0001,0.0001,0.0,0.0,0.0,0.0]},"derate_table":[[0,70,354,3998.0],[70,80,5437,4001.0],[80,90,17318,4001.0],[90,100,25404,4001.0],[100,105,9384,4001.0],[105,110,7024,4001.0],[110,115,4237,4001.0],[115,200,404,4001.0]],"per_turbine_p99":[["38B",118.8],["30A",115.5],["35B",115.0],["26D",114.5],["15A",114.4],["20C",114.1],["25D",113.6],["31A",113.4],["32E",113.0],["22C",112.9],["02E",112.9],["24C",112.8],["16B",112.6],["27D",112.3],["21C",111.8],["36B",111.7],["33A",111.7],["18B",111.4],["08F",111.2],["11F",111.1],["28D",110.7],["29A",110.3],["05C",110.2],["09F",109.9],["17B",109.6],["14D",108.8],["12F",108.8],["34A",108.7],["07F",108.7],["03E",108.6],["10F",108.6],["37B",108.4],["06F",108.4],["19C",108.0],["23C",107.6],["04E",107.6],["01E",107.4],["13E",106.1]],"params":{"blower1":80,"blower2":100,"hyst":10,"warn":130.2,"trip":155,"brg_limit":100,"heater_dT":10,"brg1":50,"brg2":70,"htrt_max":80,"red_delay":60}};
const D={"param":{"deadband":3.0,"db_off":10.0,"stop_tol":0.5,"stop_off":1.0,"delay_s":2.0,"yaw_spd_pct":73.0,"brk_lo":20.0,"brk_hi":180.0,"grad":200.0,"start_p":70.0,"pump_p":180.0,"pump_hyst":5.0,"hub_min_p":195.0,"wl1":2.8,"ratio":119.752,"tower_hz":0.351},"curve":{"rpm":[4.5,4.9,5.9,6.9,7.7,8.4,9.4,10.4,11.4,12.4,13.4,14.4,15.4,16.4,17.4],"kw":[0.0,52.0,118.0,216.0,353.0,641.0,905.0,1237.0,1673.0,2142.0,2654.0,3206.0,3797.0,4425.0,5087.0],"measured_kw":[52.0,48.0,123.0,344.0,281.0,600.0,874.0,1196.0,1613.0,2070.0,3324.0,null,null,null,null]},"rpm_hist":{"c":[4.05,4.15,4.25,4.35,4.45,4.55,4.65,4.75,4.85,4.95,5.05,5.15,5.25,5.35,5.45,5.55,5.65,5.75,5.85,5.95,6.05,6.15,6.25,6.35,6.45,6.55,6.65,6.75,6.85,6.95,7.05,7.15,7.25,7.35,7.45,7.55,7.65,7.75,7.85,7.95,8.05,8.15,8.25,8.35,8.45,8.55,8.65,8.75,8.85,8.95,9.05,9.15,9.25,9.35,9.45,9.55,9.65,9.75,9.85,9.95,10.05,10.15,10.25,10.35,10.45,10.55,10.65,10.75,10.85,10.95,11.05,11.15,11.25,11.35,11.45,11.55,11.65,11.75,11.85,11.95,12.05,12.15,12.25,12.35,12.45,12.55,12.65,12.75,12.85,12.95,13.05,13.15,13.25,13.35,13.45,13.55,13.65,13.75,13.85,13.95,14.05,14.15,14.25,14.35,14.45,14.55,14.65,14.75,14.85,14.95],"p":[0.0291,0.1249,0.2086,0.2208,0.1281,0.1616,0.3865,0.6205,0.7066,0.5416,0.5119,0.5068,0.5158,0.5592,0.5435,0.5586,0.5668,0.5789,0.6349,0.859,1.3029,1.6072,1.762,1.7632,1.3948,0.8994,0.4871,0.2469,0.1084,0.0615,0.0559,0.0558,0.06,0.0666,0.0916,0.2699,0.6362,1.1523,1.6707,1.9902,1.8222,1.8428,1.5637,1.1769,0.9526,0.83,0.7907,0.7718,0.7461,0.7273,0.7095,0.7109,0.7196,0.7348,0.7831,0.7833,0.7725,0.7554,0.7372,0.7272,0.7275,0.7211,0.7204,0.7331,0.7783,0.7847,0.78,0.7618,0.7475,0.7386,0.7208,0.7035,0.687,0.6952,0.6923,0.6846,0.6774,0.6662,0.6444,0.6245,0.5637,0.6022,0.5956,0.5783,0.5784,0.569,0.5535,0.5417,0.5276,0.537,0.6234,1.1799,4.7224,20.7975,7.552,1.4474,0.3395,0.0776,0.0162,0.0034,0.0007,0.0002,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]},"pump_hist":{"c":[141.2,143.8,146.2,148.8,151.2,153.8,156.2,158.8,161.2,163.8,166.2,168.8,171.2,173.8,176.2,178.8,181.2,183.8,186.2,188.8,191.2,193.8,196.2,198.8,201.2],"p":[0.005,0.003,0.004,0.002,0.003,0.003,0.014,0.033,0.221,0.263,0.53,0.26,0.396,0.239,3.392,36.898,57.12,0.059,0.002,0.0,0.0,0.0,0.0,0.0,0.0]},"blade":{"med":238.0,"spread":[["28D",4],["18B",4],["01E",3],["09F",2],["25D",2],["22C",2],["11F",2],["20C",2],["05C",2],["24C",1],["37B",1],["34A",1],["33A",1],["32E",1],["31A",1],["30A",1],["29A",1],["03E",1],["04E",1],["10F",1],["23C",1],["38B",1],["21C",1],["02E",1],["19C",1],["06F",1],["17B",1],["16B",1],["15A",1],["14D",1],["13E",1],["12F",1],["07F",1],["26D",0],["27D",0],["35B",0],["36B",0],["08F",0]]},"yaw_knee":{"c":[0.125,0.375,0.625,0.875,1.125,1.375,1.625,1.875,2.125,2.375,2.625,2.875,3.125,3.375,3.625,3.875,4.125,4.375,4.625,4.875,5.125,5.375,5.625,5.875,6.125,6.375,6.625,6.875,7.125,7.375,7.625,7.875],"P":[76.33,75.91,75.62,75.15,75.01,75.19,76.33,78.99,83.95,90.45,96.77,97.08,99.81,99.79,99.82,99.76,99.83,99.55,99.68,97.04,99.61,99.62,99.44,99.03,99.4,99.13,99.5,99.42,98.58,98.59,99.37,98.31]},"unw_hist":{"c":[0.05,0.15,0.25,0.35,0.45,0.55,0.65,0.75,0.85,0.95,1.05,1.15,1.25,1.35,1.45,1.55,1.65,1.75,1.85,1.95,2.05,2.15,2.25,2.35,2.45,2.55,2.65,2.75,2.85,2.95,3.05,3.15],"p":[0.079,0.039,0.039,0.039,2.25,7.304,8.133,10.225,11.607,15.041,19.7,9.159,5.645,3.277,1.816,1.856,1.54,0.908,0.474,0.316,0.513,0.0,0.0,0.039,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"med":0.97,"n":2533},"stats":{"knee_med":3.0,"knee_sd":0.114,"knee_n":38,"knee_dist":{"2.75":1,"3.0":29,"3.25":8},"notch_ratio":0.06,"notch_rpm":7.02,"quiet_p99":2.31,"pump_band":98.1,"pump_lo":0.04,"blade_max":4,"unw_per_turbine":66,"turns_per_turbine":677,"days":553}};
const P=D.param, S={t:0,ws:7.5,wd:120,wdf:120,nac:120,yawing:0,dly:0,brk:180,twist:0,unw:0,unwing:0,
rpm:0,kw:0,pitch:0,pump:0,pumpOn:0,bl:[238,238,238],hist:[],uev:[],rail:241,rload:0,rh:[],rhT:0,pitPrev:0,Tst:35,Tnac:25,blow:0,Th:[],ThT:0};
const $=s=>document.querySelector(s), mk=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
const CV=(id,d)=>{const e=$("#"+id);return e?+e.value:d};
const wrap=x=>((x+180)%360+360)%360-180, clamp=(x,a,b)=>x<a?a:x>b?b:x;
const NOTCH_C=P.tower_hz*60/3, NB=[NOTCH_C-0.15,NOTCH_C+0.55];
/* ---------- verdict chips ---------- */
const VD=[["Y","偏航死区","已降级","warn","3.00°为分箱伪影"],["Y","偏航泵压","身份改判","warn","非刹车钳压力"],
["Y","解缆触发","0.97 圈","ok","实测定值"],["Y","偏航延时","2 s 不可测","nil","低于 Nyquist"],
["D","3P 禁带","7.02 rpm","ok","密度比 0.06"],["D","转矩-转速","−3~−4%","ok","中段贴合"],
["G","热降额损失","0","ok","额定以上风功率不塌"],["G","热裕度","峰值余量 3.0°C","ok","越告警线 0.0015%(46 格 / 4 台)"],
["P","三桨对称","已撤回","bad","共轨非独立回路"]];
const LT={yaw:"Y",drv:"D",gen:"G",pit:"P"};
$("#chips").append(...VD.filter(v=>!SF||v[0]===LT[SF]).map(([s,a,b,c,d])=>mk("div","chip c-"+c,
`<span class="dot"></span><span class="sy">${s}</span>${a} <b>${b}</b> <span style="color:var(--ink3);font-size:11.5px">${d}</span>`)));
/* ---------- canvas helper ---------- */
function cv(el,w,h){const d=Math.min(devicePixelRatio||1,2);el.width=w*d;el.height=h*d;
el.style.width=w+"px";el.style.height=h+"px";const x=el.getContext("2d");x.scale(d,d);x._w=w;x._h=h;return x}
const CS=()=>getComputedStyle(document.body), V=n=>CS().getPropertyValue(n).trim();
function axes(g,pad,xr,yr,xl,yl,xt,yt){const w=g._w,h=g._h,L=pad[3],R=w-pad[1],T=pad[0],B=h-pad[2];
g.clearRect(0,0,w,h);g.strokeStyle=V("--line2");g.lineWidth=1;g.fillStyle=V("--ink3");
g.font='500 10px "IBM Plex Mono",monospace';
const X=v=>L+(v-xr[0])/(xr[1]-xr[0])*(R-L), Y=v=>B-(v-yr[0])/(yr[1]-yr[0])*(B-T);
(yt||[]).forEach(v=>{const y=Y(v);g.beginPath();g.moveTo(L,y);g.lineTo(R,y);g.stroke();
g.textAlign="right";g.textBaseline="middle";g.fillText(String(v),L-6,y)});
(xt||[]).forEach(v=>{const x=X(v);g.textAlign="center";g.textBaseline="top";g.fillText(String(v),x,B+6)});
g.strokeStyle=V("--line");g.beginPath();g.moveTo(L,T);g.lineTo(L,B);g.lineTo(R,B);g.stroke();
g.fillStyle=V("--ink3");g.font='500 10.5px "IBM Plex Sans Condensed",sans-serif';
if(xl){g.textAlign="right";g.textBaseline="bottom";g.fillText(xl,R,h-1)}
if(yl){g.save();g.translate(9,T);g.rotate(-Math.PI/2);g.textAlign="right";g.textBaseline="top";g.fillText(yl,0,0);g.restore()}
return {X,Y,L,R,T,B}}
/* ---------- panels ---------- */
const PN=[], SYS={
yaw:["Y","偏航系统","NacelPos · ActualWindDirection · ScYawPos · ScYawOpe · 偏航制动压力",
"<em>1</em> 降级 · <em>1</em> 通道纠正 · <em>2</em> 不可测"],
drv:["D","传动链与功率系统","generator_speed · active_power · 塔筒一阶(CMS)",
"<em>2</em> 确证 · 两条独立测量链互证"],
gen:["G","发电机与热管理","Gen1U/V/W1Tm 定子三相 · GenBeG/GenBeR 轴承 · AmbieTmp · NacelTmp",
"<em>1</em> 确证(热降额损失=0) · <em>1</em> 未验证(三相对照组)"],
pit:["P","变桨与轮毂液压系统","桨叶A/B/C油压(同一共轨的三个取压点) · 液压站预充压力",
"<em>1</em> 撤回 — 原判据灵敏度为零"]};
const SB={};
function bay(k){if(SB[k])return SB[k];
const [n,t,c,s]=SYS[k], sec=mk("section","sys");
sec.append(mk("div","sysh",`<span class="n">${n}</span><h2>${t}</h2><span class="ch">${c}</span><span class="sm">${s}</span>`));
const g=mk("div","grid");sec.append(g);$("#stack").append(sec);return SB[k]=g}
function panel(o){if(SF&&o.sys!==SF)return;const p=mk("section","panel"+(o.wide?" wide":""));
p.append(mk("div","ph",`<h2>${o.t}</h2><span class="code">${o.c}</span><span class="vd vd-${o.vc}">${o.v}</span>`));
const b=mk("div","pb"+(o.stack?" stack":""));
const s=mk("canvas","sim"),r=mk("div","rd");b.append(s,r);p.append(b);
if(o.note)p.append(mk("div","note",o.note));
bay(o.sys).append(p);const g=cv(s,o.w,o.h);
o.rows.forEach(x=>r.append(mk("div","row "+(x[2]||""),
`<span class="k">${x[0]}</span><span class="v" data-f="${x[1]}">–</span>`)));
PN.push({...o,g,r});return p}
/* P1 偏航 */
panel({sys:"yaw",d:drawYaw,t:"偏航对风控制",c:"UP10239 / UP10204 / UP10242",v:"原 3.00° 已降级",vc:"warn",w:262,h:262,
rows:[["死区(并网)","db","s"],["启动延时","dly","s"],["偏航速度","spd","s"],["停止容差","tol","s"],
["",'sep'],["对风偏差 |err|","err","m"],["机舱朝向","nac","m"],["刹车压力","brk","m"],["状态","st","m"]],
note:'<b style="color:var(--warn)">原结论「死区 3.00° 确证」已降级——外部双模审逮到分箱伪影,data-first 复现证实。</b><br>估计值随箱宽单调滑动:0.25° 箱 → 中位 <b>3.000°</b>;0.10° 箱 → 2.850°;0.02° 箱 → <b>2.650°</b>。细箱下 <b>0/38 台</b>落在 3.00° 的 ±0.10° 内。机理:判据「静默率首次≤1% 的首格」是上尾判据,箱越宽越把过渡区与低 |err| 样本平均在一起,交叉点被推高——0.25° 箱的右边界恰为 3.00,与参数表的吻合是分箱制造的。<br><b>存活的</b>:细箱 sd 0.0908 远大于分箱伪影预期 0.0058 → 跨台 ~0.09° 散布为真;分布单峰;死区量级 O(3°) 成立。<b>不存活的</b>:所述精度上的「与参数表一致」。10min 均值口径的拐点到瞬时阈值的映射需正向削峰模型,未验证——<b>不得事后合理化为「削峰所以等价」</b>。<br><b>方法上仍成立</b>:<code>err = wrap(风向 − 机舱位置)</code> 两列同属机舱编码器基准,做差抵消未对北 offset。'});
/* P2 转矩-转速 */
panel({sys:"drv",d:drawCurve,t:"转矩–转速控制律",c:"UP02651-65 十五点指令表",v:"中段 −3~−4%",vc:"ok",stack:1,w:512,h:250,
rows:[["指令曲线","cv","s"],["当前转速","rpm","m"],["当前功率","kw","m"],["桨距角","pit","m"],
["",'sep'],["实测/指令 中段","dev","m"],["3P 禁带两侧","dev2","m"]],
note:'<b>预言</b>:稳态工作点应落在十五点指令表上。<b>实测</b>:8.4–12.4 rpm 段中位偏低 3–4%(发电机与变流器效率),贴合。<b>唯二的大偏差 6.9 rpm(+59%)与 7.7 rpm(−20%)正好夹着 7.02 rpm 的 3P 禁带</b>——那两点上机器不稳态停留,是冲过去的。转矩曲线的异常和共振禁带互相印证。'});
/* P3 3P */
panel({sys:"drv",d:drawNotch,t:"塔筒 3P 共振禁带",c:"塔筒一阶 0.351 Hz ÷ 3",v:"密度比 0.06",vc:"ok",stack:1,w:512,h:250,
rows:[["塔筒一阶(CMS)","tw","s"],["3P 共振转速","n3","s"],["禁带","nb","s"],
["",'sep'],["TSR 最优转速","tsr","m"],["实际转速","rpm3","m"],["禁带/邻带密度比","ratio","m"]],
note:'<b>预言</b>:塔筒 0.351 Hz(来自 CMS 加速度计,与 SCADA <b>完全独立的测量链</b>)÷3×60 = 7.02 rpm 处应有转速禁带。<b>实测</b>:7.05 rpm 密度 0.06%/0.1rpm vs 邻带 1.07%,深 30 倍;两侧各有 1.2–1.35× 基线的堆积 = 禁带双肩签名。<br><b>陷阱</b>:<code>rotor_speed</code> 原列仅 17 个唯一值(1 rpm 量化),直接用它做不出这个结论;转速须由 <code>generator_speed ÷ 119.752</code> 反量化到 0.0084 rpm。'});
/* P4 泵 */
panel({sys:"yaw",d:drawPump,t:"偏航液压泵压力",c:"UP07211 = 180 bar / UP07212 = 5 bar 迟滞",v:"通道身份纠正",vc:"warn",stack:1,w:512,h:250,
rows:[["泵工作压力","pp","s"],["迟滞","ph","s"],["刹车两电平(钳)","bl2","s"],
["",'sep'],["当前供油压力","pcur","m"],["泵状态","pst","m"],["175–185 bar 占比","pband","m"],["15–25 bar 占比","plo","m"]],
note:'<b>我的预言错了。</b>我按列名猜这是刹车钳压力,应呈低/高两电平。<b>实测</b>:低压带(15–25 bar)只占 <code>0.04%</code>,20 bar 那一档根本不存在;98.1% 的质量压在 175–185 bar,且 40%/57% 劈在 180 两侧。<br>这是 <b>UP07211 = 180 bar 泵工作压力 ± UP07212 = 5 bar 迟滞</b> 的锯齿带。<b>该列是偏航液压泵的供油压力,不是刹车钳压力</b>——数据纠正了通道身份。'});
/* G1 发电机温升 */
panel({sys:"gen",d:drawTherm,t:"发电机温升",c:"UP02212/13 风机两级 · UP02110 告警 · UP02102 保护",v:"模型 R²=0.84",vc:"warn",stack:1,w:512,h:250,
rows:[["稳态温升模型","mdl","s"],["空载分量 A(铁损+风摩)","A","s"],["铜损分量 B(∝P²)","Bc","s"],["防凝露加热器温差","htr","s"],
["",'sep'],["机舱温度","tnac","m"],["定子最热相","tst","m"],["当前温升 ΔT","dtn","m"],["冷却风机档","blw","m"],["距绕组保护余量","marg","m"]],
note:'<b>模型</b>:ΔT = A + B·(P/Pn)²,实测稳态拟合 <b>A=24.8°C</b>(铁损+风摩,与功率无关)、<b>B=42.2°C</b>(铜损 ∝ I²R),满载总温升 <b>67.0°C</b>(R²=0.84,RMS 5.9°C,n=84 万条 4 小时稳态窗)。<br><b>参数与数据对上的一处</b>:零载时实测 ΔT 仍有 14.0°C —— 那不是损耗,是 <code>UP02200 防凝露加热器</code>,温差设定 10.0°C。<br><b>限定</b>:τ≈139 min 是 (定子−舱温) 在 10min 口径下的拟合滞后,<b>混进了机舱热质量,不是绕组自身时间常数</b>,拆开需更快的层。拟合曲线是<b>已含冷却风机动作</b>的"如实运行"曲线,风机档位只作状态指示,未另建冷却模型(无停风机数据可标定,不编)。'});
/* G2 热裕度 */
panel({sys:"gen",d:drawMargin,t:"热裕度与热降额",c:"UP01925 HTRT · UP02807 降额延时",v:"热降额损失 = 0",vc:"ok",stack:1,w:512,h:250,
rows:[["热降额上限 HTRT","htrt","s"],["降额输出延时","rdel","s"],["绕组保护 / 轴承限值","lims","s"],
["",'sep'],["越 80°C(风机1级)","a80","m"],["越 130.2°C(高温告警 = 155°C × 84%)","a84","m"],["越 100°C(风机2级)","a100","m"],["实测峰值 / 余量","pk","m"],["额定以上风的功率","dr","m"]],
note:'<b>问题</b>:热造成多少功率损失?<b>答案:0。</b><br><b>设计</b>:条件在<b>额定以上风速(>13 m/s)且未限电(PowerRef 满档)</b> —— 那里功率本应恒等于额定、与温度无关,除非热降额。这样绕开方向陷阱(<b>温度高本来就是因为功率高</b>,直接拿温度回归功率缺额会得到反号结果)。<br><b>结果</b>:从 <70°C 到 115–200°C,<b>每一个温度区间的功率中位都是 4001 kW</b>,p10 也不塌。热降额未发生。<br>机组热行为正常。冷却系统按设计工作:22.7% 的时间越 80°C(风机 1 级)、3.3% 越 100°C(风机 2 级),这是冷却门限,不是告警。绕组高温告警线为 155°C × 84% = 130.2°C(定值表 UP02110 的单位是 %,与 UP01900 HTRT 130.0°C 独立吻合);越线仅 0.0015%(46 格 / 4 台:09F 26、22C 9、06F 8、25D 3)。实测瞬时峰值 152.0°C(09F),距 155°C 保护 3.0°C,属个体极值。'});
/* P5 三桨 */
panel({sys:"pit",d:drawRail,t:"变桨液压主回路(单节点共轨)",c:"图纸 1034384/1034386 · UP07200/07228 = 245/195 bar",v:"原结论已撤回",vc:"bad",stack:1,w:512,h:250,
rows:[["卸荷 / 加载门限","rl","s"],["泵站 70 号蓄能器","ac1","s"],["轮毂 106 蓄能器","ac2","s"],["耦合系数 kH","kh","s"],
["",'sep'],["共轨压力","rail","m"],["A / B / C 取压点","abc","m"],["三点极差(本刻)","spr","m"],["三点相关(实测)","corr","m"],["跨点差 p99 / max","exc","m"]],
note:'<b style="color:var(--bad)">原结论「38 台三桨对称 · 健康清场」已撤回。</b>该列不是叶片压力,是<b>液压主回路压力</b>:变桨液压为<b>单节点共轨</b>(泵持续运转 + 25 号阀滞环卸荷 + 双蓄能器 —— 泵站 70 号 10L/180bar 近端缓冲、轮毂 106 6×30L/125bar 顺桨主源),<b>不存在三条可互为对照的独立桨回路</b>。<br>错了三层:<b>①物理模型</b>——共轨上「跨桨压差≈0」是必然,不是健康证据;<b>②统计量</b>——用逐台中位数抹掉时间维,同数据 p99 跨点差 16–20 bar、max 167 bar 被吃掉;<b>③与地面真值冲突</b>——现场已证实 8#(70 号失氮,4# 有换件先例)、25#(泄漏 2.8×)、14#/32#(泵间歇失供),在 ≥4 台确诊故障的机队上判「全健康」= <b>判据灵敏度为零</b>。<br><b>真判据是共轨的充放循环签名</b>(吞吐 L/周期 · 周期 s · 带宽 bar;8# 实测带宽 135 / 周期 2.6 s,C 从健康 0.087 L/bar 塌到 0.0037)。<b>周期 2.6 s 在 1min 层完全混叠</b>,本层不可测,须 fastlog 层——且已由 <code>U6_变桨液压仿真台</code>(图纸 1034384/1034386 + 2026-08-13 现场核实)做完,此处不重造。'});
/* P6 解缆 */
panel({sys:"yaw",d:drawTwist,t:"偏航解缆循环",c:"ScYawPos 累计角(无 0/360 环绕)",v:"触发 0.97 圈",vc:"ok",w:262,h:262,
rows:[["强制解缆周期","fy","s"],["硬限位(实测)","hl","s"],
["",'sep'],["当前扭缆","tw2","m"],["累计偏航行程","trav","m"],["解缆触发中位","um","m"],["全场解缆频次","uf","m"]],
note:'<b>触发点不是 ±3 圈,是 ±1 圈。</b>2533 次解缆事件,触发时累计角中位 <code>0.97 圈</code>(p25 0.76 / p75 1.10),仅 0.6% 拖过 2 圈;硬限位在 2–3 圈(跨台峰值中位 2.08、最大 2.99)。<br>全场解缆中位 66 次/台、累计偏航行程 677 圈/台(553 天)= <b>每台每天转 1.2 圈</b>——这个量就是偏航轴承的磨损剂量流水。'});
/* ---------- physics ---------- */
function step(dt){
const tb=CV("tb",1);
S.t+=dt;
// 风向随机游走 + 阵风
S.wd=(S.wd+ (Math.sin(S.t*0.07)*0.9+Math.sin(S.t*0.23)*0.5+(Math.random()-0.5)*2.2)*tb*dt +360)%360;
S.ws=CV("ws",7.5);
// 偏航系统滤波 (UP10291-94: 低风 400s / 高风 200s)
const tc=S.ws<4?400:(S.ws>6?200:300);
S.wdf=(S.wdf+wrap(S.wd-S.wdf)*(1-Math.exp(-dt/tc))+360)%360;
// 偏航状态机
const err=wrap(S.wdf-S.nac), canYaw=S.ws>=P.wl1;
if(!S.unwing){
if(!S.yawing){
if(Math.abs(err)>P.deadband&&canYaw){S.dly+=dt; if(S.dly>=P.delay_s){S.yawing=Math.sign(err);S.dly=0}}
else S.dly=0;
}else{
const st=S.yawing*(P.yaw_spd_pct/100)*0.5*dt;
S.nac=(S.nac+st+360)%360; S.twist+=st;
const e2=wrap(S.wdf-S.nac);
if(Math.abs(e2)<=P.stop_tol+P.stop_off||Math.sign(e2)!==S.yawing)S.yawing=0;
}
}
// 解缆: 实测触发 0.97 圈
if(!S.unwing&&Math.abs(S.twist)>=349){S.unwing=Math.sign(S.twist);S.yawing=0;S.unw++;S.uev.push(Math.abs(S.twist)/360)}
if(S.unwing){const st=-S.unwing*1.0*dt;S.nac=(S.nac+st+360)%360;S.twist+=st;
if(Math.abs(S.twist)<1.5){S.twist=0;S.unwing=0}}
// 刹车压力 (梯度 + 起偏航门限)
S.brk = (S.yawing||S.unwing) ? Math.max(P.brk_lo,S.brk-P.grad*dt) : Math.min(P.brk_hi,S.brk+P.grad*dt);
// 泵: 175-180 迟滞锯齿, 偏航时耗油更快
const leak=(S.yawing||S.unwing)?7.5:1.1;
if(S.pumpOn){S.pump+=11*dt; if(S.pump>=P.pump_p)S.pumpOn=0}
else{S.pump-=leak*dt; if(S.pump<=P.pump_p-P.pump_hyst)S.pumpOn=1}
S.pump=clamp(S.pump,168,186);
S.hist.push(S.pump); if(S.hist.length>240)S.hist.shift();
// 转子: TSR 最优 + 禁带排斥 + 额定限功
let r=clamp(1.175*S.ws,0,14.4);
if(S.ws<3)r=0; else if(r<4.5)r=4.5;
S.tsr=r;
if(r>NB[0]&&r<NB[1]) r = (r-NB[0] < NB[1]-r) ? NB[0] : NB[1];
S.rpm+=(r-S.rpm)*(1-Math.exp(-dt/8));
const cs=D.curve.rpm,cp=D.curve.kw; let k=0;
for(let i=0;i<cs.length-1;i++) if(S.rpm>=cs[i])k=i;
const f=clamp((S.rpm-cs[k])/(cs[k+1]-cs[k]||1),0,1);
S.kw=Math.min(3999,cp[k]+(cp[k+1]-cp[k])*f);
S.pitch = S.ws>12.5 ? clamp((S.ws-12.5)*2.6,0,28) : (S.ws<3?89:0.2);
// 变桨液压共轨: 泵持续运转 + 25 号阀滞环卸荷 (UP07200/07228 = 245/195), 单节点
const dpit=Math.abs(S.pitch-S.pitPrev); S.pitPrev=S.pitch;
const use=2.2/60 + dpit*0.9; // 基线泄漏 2.2 L/min + 变桨消耗
const CAP=0.087; // 健康总容 0.087 L/bar (70号全额 + 0.095×106)
if(S.rload){S.rail+=(0.62-use)/CAP*dt; if(S.rail>=245)S.rload=0}
else{S.rail-=use/CAP*dt; if(S.rail<=236)S.rload=1}
S.rail=clamp(S.rail,232,248);
S.rhT+=dt; if(S.rhT>=4){S.rhT=0;S.rh.push(S.rail); if(S.rh.length>200)S.rh.shift()} // 按仿真时间采样→窗口 800s 覆盖多个周期
// ── 发电机热模型: ΔT = A + B·(P/Pn)² (稳态实测拟合, 已含风机staging), 一阶滞后 τ
const amb=CV("amb",18);
S.Tnac += (amb+6 - S.Tnac)*(1-Math.exp(-dt/900)); // 舱温跟环温 + 内部散热抬升
const pu=S.kw/3999;
const dTss=Math.max(TH.params.heater_dT, TH.A_noload + TH.B_copper*pu*pu); // 零载下限=防凝露加热器
const Tss=S.Tnac+dTss;
S.Tst += (Tss-S.Tst)*(1-Math.exp(-dt/(TH.tau_min*60)));
const P0=TH.params; // 风机两级 + 滞环
if(S.blow<2&&S.Tst>=P0.blower2)S.blow=2;
else if(S.blow<1&&S.Tst>=P0.blower1)S.blow=1;
else if(S.blow==2&&S.Tst<P0.blower2-P0.hyst)S.blow=1;
else if(S.blow==1&&S.Tst<P0.blower1-P0.hyst)S.blow=0;
S.ThT+=dt; if(S.ThT>=120){S.ThT=0;S.Th.push(S.Tst); if(S.Th.length>200)S.Th.shift()}
// 三个取压点 = 同一节点 + 各自传感器偏置/噪声 (实测相关 0.95)
for(let i=0;i<3;i++)S.bl[i]=Math.round(S.rail-3+[0,-1,-2][i]+Math.sin(S.t*0.3+i*2.1)*1.1+(Math.random()-0.5)*1.6);
}
/* ---------- draw ---------- */
function turbine(g,cx,cy,R,head,pitch){
g.save();g.translate(cx,cy);g.rotate(head*Math.PI/180);
g.strokeStyle=V("--ink2");g.lineWidth=2.5;g.lineCap="round";
g.beginPath();g.moveTo(0,10);g.lineTo(0,-R*0.5);g.stroke(); // 机舱
g.fillStyle=V("--ink");g.beginPath();g.arc(0,-R*0.5,4.5,0,7);g.fill(); // 轮毂
g.strokeStyle=V("--spec");g.lineWidth=3;
for(let i=0;i<3;i++){const a=(S.t*S.rpm*6+i*120)*Math.PI/180;
g.save();g.translate(0,-R*0.5);g.rotate(a);
g.beginPath();g.moveTo(0,0);g.lineTo(0,-R*0.42);g.stroke();
g.fillStyle=V("--spec");g.globalAlpha=.55;
const bw=2+Math.cos(pitch*Math.PI/180)*3.4;
g.beginPath();g.ellipse(0,-R*0.3,bw,R*0.13,0,0,7);g.fill();g.globalAlpha=1;g.restore()}
g.restore()}
function drawYaw(g){const w=g._w,h=g._h,cx=w/2,cy=h/2,R=w/2-14;
g.clearRect(0,0,w,h);
g.strokeStyle=V("--line");g.lineWidth=1;g.beginPath();g.arc(cx,cy,R,0,7);g.stroke();
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Mono",monospace';g.textAlign="center";g.textBaseline="middle";
for(let a=0;a<360;a+=30){const rad=(a-90)*Math.PI/180;
g.beginPath();g.moveTo(cx+Math.cos(rad)*R,cy+Math.sin(rad)*R);
g.lineTo(cx+Math.cos(rad)*(R-(a%90?4:7)),cy+Math.sin(rad)*(R-(a%90?4:7)));g.stroke();
if(a%90===0)g.fillText(["N","E","S","W"][a/90],cx+Math.cos(rad)*(R+8),cy+Math.sin(rad)*(R+8))}
// 死区锥
const wf=(S.wdf-90)*Math.PI/180, db=P.deadband*Math.PI/180;
g.fillStyle=V("--specf");g.beginPath();g.moveTo(cx,cy);
g.arc(cx,cy,R-3,wf-db,wf+db);g.closePath();g.fill();
g.strokeStyle=V("--spec");g.setLineDash([3,3]);g.lineWidth=1;
[-db,db].forEach(o=>{g.beginPath();g.moveTo(cx,cy);
g.lineTo(cx+Math.cos(wf+o)*(R-3),cy+Math.sin(wf+o)*(R-3));g.stroke()});g.setLineDash([]);
// 风矢 (瞬时 + 滤波)
const wi=(S.wd-90)*Math.PI/180;
g.strokeStyle=V("--ink3");g.lineWidth=1.5;g.globalAlpha=.5;
g.beginPath();g.moveTo(cx+Math.cos(wi)*R,cy+Math.sin(wi)*R);g.lineTo(cx+Math.cos(wi)*(R-26),cy+Math.sin(wi)*(R-26));g.stroke();g.globalAlpha=1;
g.strokeStyle=V("--meas");g.lineWidth=2.5;
g.beginPath();g.moveTo(cx+Math.cos(wf)*R,cy+Math.sin(wf)*R);g.lineTo(cx+Math.cos(wf)*(R-34),cy+Math.sin(wf)*(R-34));g.stroke();
g.fillStyle=V("--meas");g.beginPath();
g.moveTo(cx+Math.cos(wf)*(R-34),cy+Math.sin(wf)*(R-34));
[-0.4,0.4].forEach(o=>g.lineTo(cx+Math.cos(wf+o)*(R-22),cy+Math.sin(wf+o)*(R-22)));g.fill();
// 误差弧
const err=wrap(S.wdf-S.nac);
if(Math.abs(err)>0.4){g.strokeStyle=Math.abs(err)>P.deadband?V("--bad"):V("--ink2");
g.lineWidth=3;g.beginPath();
g.arc(cx,cy,R*0.62,(S.nac-90)*Math.PI/180,wf,err<0);g.stroke()}
turbine(g,cx,cy,R*0.78,S.nac,S.pitch);
g.fillStyle=(S.yawing||S.unwing)?V("--bad"):V("--ink3");
g.font='600 10px "IBM Plex Mono",monospace';g.textAlign="center";
g.fillText(S.unwing?"UNTWIST":(S.yawing?"YAWING":(S.dly>0?"DELAY "+S.dly.toFixed(1)+"s":"HOLD")),cx,h-5)}
function drawCurve(g){const cs=D.curve.rpm,cp=D.curve.kw,ms=D.curve.measured_kw;
const A=axes(g,[14,14,26,42],[4,17.6],[0,5200],"转子转速 rpm","kW",[5,7,9,11,13,15,17],[0,1000,2000,3000,4000,5000]);
g.fillStyle=V("--bad")+"1f";g.fillRect(A.X(NB[0]),A.T,A.X(NB[1])-A.X(NB[0]),A.B-A.T);
g.fillStyle=V("--bad");g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="center";
g.fillText("3P",A.X((NB[0]+NB[1])/2),A.T+9);
g.strokeStyle=V("--spec");g.lineWidth=2;g.beginPath();
cs.forEach((s,i)=>i?g.lineTo(A.X(s),A.Y(cp[i])):g.moveTo(A.X(s),A.Y(cp[i])));g.stroke();
g.fillStyle=V("--spec");cs.forEach((s,i)=>{g.beginPath();g.arc(A.X(s),A.Y(cp[i]),2.5,0,7);g.fill()});
g.fillStyle=V("--meas");ms.forEach((v,i)=>{if(v==null)return;
g.beginPath();g.arc(A.X(cs[i]),A.Y(v),3.6,0,7);g.fill()});
if(S.rpm>4){g.fillStyle=V("--ink");g.strokeStyle=V("--panel");g.lineWidth=2;
g.beginPath();g.arc(A.X(S.rpm),A.Y(S.kw),5.5,0,7);g.fill();g.stroke()}
g.font='500 10px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";
g.fillStyle=V("--spec");g.fillText("● 指令表 UP02651-65",A.L+8,A.T+12);
g.fillStyle=V("--meas");g.fillText("● 实测中位",A.L+8,A.T+26)}
function drawNotch(g){const H=D.rpm_hist;
const A=axes(g,[14,14,26,42],[4,15],[0,2.3],"转子转速 rpm","%/0.1rpm",[5,6,7,8,9,10,11,12,13,14],[0,0.5,1,1.5,2]);
g.fillStyle=V("--bad")+"1f";g.fillRect(A.X(NB[0]),A.T,A.X(NB[1])-A.X(NB[0]),A.B-A.T);
g.beginPath();g.moveTo(A.X(H.c[0]),A.B);
H.c.forEach((c,i)=>g.lineTo(A.X(c),A.Y(Math.min(H.p[i],2.3))));
g.lineTo(A.X(H.c[H.c.length-1]),A.B);g.closePath();
g.fillStyle=V("--measf");g.fill();g.strokeStyle=V("--meas");g.lineWidth=1.6;
g.beginPath();H.c.forEach((c,i)=>i?g.lineTo(A.X(c),A.Y(Math.min(H.p[i],2.3))):g.moveTo(A.X(c),A.Y(Math.min(H.p[i],2.3))));g.stroke();
g.strokeStyle=V("--spec");g.lineWidth=1.5;g.setLineDash([4,3]);
g.beginPath();g.moveTo(A.X(NOTCH_C),A.T);g.lineTo(A.X(NOTCH_C),A.B);g.stroke();g.setLineDash([]);
g.fillStyle=V("--spec");g.font='600 9.5px "IBM Plex Mono",monospace';g.textAlign="left";
g.fillText("0.351Hz÷3 = 7.02",A.X(NOTCH_C)+5,A.T+10);
if(S.rpm>4){const inb=S.tsr>NB[0]&&S.tsr<NB[1];
if(inb){g.strokeStyle=V("--ink3");g.lineWidth=1;g.setLineDash([2,2]);
g.beginPath();g.moveTo(A.X(S.tsr),A.Y(0));g.lineTo(A.X(S.tsr),A.Y(2.2));g.stroke();g.setLineDash([]);
g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="center";
g.fillText("TSR最优 被推开",A.X(S.tsr),A.Y(2.2)-4)}
g.fillStyle=V("--ink");g.strokeStyle=V("--panel");g.lineWidth=2;
g.beginPath();g.arc(A.X(S.rpm),A.B-6,5.5,0,7);g.fill();g.stroke()}}
function drawPump(g){const w=g._w,h=g._h,sw=w*0.56;
g.clearRect(0,0,w,h);
// 左: 实时锯齿
const T=14,B2=h-26,L=34,R2=sw-8, yv=v=>B2-(v-168)/(186-168)*(B2-T);
g.strokeStyle=V("--line2");g.lineWidth=1;g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Mono",monospace';
[170,175,180,185].forEach(v=>{g.beginPath();g.moveTo(L,yv(v));g.lineTo(R2,yv(v));g.stroke();
g.textAlign="right";g.textBaseline="middle";g.fillText(v,L-5,yv(v))});
g.fillStyle=V("--specf");g.fillRect(L,yv(P.pump_p),R2-L,yv(P.pump_p-P.pump_hyst)-yv(P.pump_p));
g.strokeStyle=V("--spec");g.setLineDash([4,3]);g.lineWidth=1.2;
[P.pump_p,P.pump_p-P.pump_hyst].forEach(v=>{g.beginPath();g.moveTo(L,yv(v));g.lineTo(R2,yv(v));g.stroke()});g.setLineDash([]);
g.strokeStyle=V("--meas");g.lineWidth=1.8;g.beginPath();
S.hist.forEach((v,i)=>{const x=L+i/239*(R2-L);i?g.lineTo(x,yv(v)):g.moveTo(x,yv(v))});g.stroke();
g.fillStyle=V("--ink3");g.font='500 10px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";g.textBaseline="top";
g.fillText("实时供油压力 bar",L,h-18);
g.fillStyle=S.pumpOn?V("--ok"):V("--ink3");g.textAlign="right";
g.fillText(S.pumpOn?"● 泵 ON":"○ 泵 OFF",R2,h-18);
// 右: 实测直方
const H=D.pump_hist,L3=sw+40,R3=w-10,mx=Math.max(...H.p);
g.strokeStyle=V("--line");g.lineWidth=1;g.beginPath();g.moveTo(L3,T);g.lineTo(L3,B2);g.stroke();
H.c.forEach((c,i)=>{const y=yv(c),bw=(R3-L3)*H.p[i]/mx;
g.fillStyle=H.p[i]>1?V("--meas"):V("--meas")+"66";
g.fillRect(L3,y-2.6,Math.max(bw,0.6),5.2)});
g.fillStyle=V("--ink3");g.font='500 10px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";
g.fillText("实测分布 2984 万点",L3,h-18);
g.fillStyle=V("--meas");g.font='600 9.5px "IBM Plex Mono",monospace';
g.fillText("98.1% 落此带",L3+4,T+2)}
function drawTherm(g){const w=g._w,h2=g._h;
g.clearRect(0,0,w,h2);
const P0=TH.params, T0=0,T1=160, BL=54,BR=w*0.34,TT=16,BB=h2-24;
const yv=v=>BB-(v-T0)/(T1-T0)*(BB-TT);
// 阈值梯
[[P0.trip,"--bad","绕组保护 155"],[P0.blower2,"--warn","风机2级 100"],[P0.warn,"--warn","高温告警 84"],
[P0.blower1,"--spec","风机1级 80"]].forEach(([v,c,lb])=>{
g.strokeStyle=V(c);g.setLineDash([4,3]);g.lineWidth=1.2;
g.beginPath();g.moveTo(BL-8,yv(v));g.lineTo(BR+70,yv(v));g.stroke();g.setLineDash([]);
g.fillStyle=V(c);g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="left";g.textBaseline="bottom";
g.fillText(lb,BR+4,yv(v)-2)});
// 温度柱
const bx=BL, bw=30;
g.fillStyle=V("--line2");g.fillRect(bx,TT,bw,BB-TT);
const marg=P0.trip-S.Tst;
const col=marg<15?"--bad":(S.Tst>P0.warn?"--warn":"--meas");
g.fillStyle=V(col);g.fillRect(bx,yv(S.Tst),bw,BB-yv(S.Tst));
// 舱温基线
g.strokeStyle=V("--ink3");g.lineWidth=1.5;
g.beginPath();g.moveTo(bx-6,yv(S.Tnac));g.lineTo(bx+bw+6,yv(S.Tnac));g.stroke();
g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="right";g.textBaseline="middle";
g.fillText("舱温",bx-8,yv(S.Tnac));
g.fillStyle=V(col);g.font='600 15px "IBM Plex Mono",monospace';g.textAlign="center";g.textBaseline="bottom";
g.fillText(S.Tst.toFixed(0)+"°C",bx+bw/2,yv(S.Tst)-5);
// 风机档
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";g.textBaseline="top";
g.fillText("冷却风机",bx-40,BB+6);
for(let i=0;i<2;i++){g.fillStyle=S.blow>i?V("--ok"):V("--line");
g.beginPath();g.arc(bx+16+i*16,BB+16,5,0,7);g.fill()}
// 右: 实测分布
const HL=w*0.56,HR=w-14,mx=Math.max(...TH.Thist.p);
g.strokeStyle=V("--line");g.lineWidth=1;g.beginPath();g.moveTo(HL,TT);g.lineTo(HL,BB);g.stroke();
TH.Thist.c.forEach((c,i)=>{const y=yv(c),bwd=(HR-HL)*TH.Thist.p[i]/mx;
g.fillStyle=c>P0.warn?V("--warn"):V("--meas");g.globalAlpha=c>P0.warn?0.85:0.45;
g.fillRect(HL,y-1.8,Math.max(bwd,0.5),3.6);g.globalAlpha=1});
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";g.textBaseline="top";
g.fillText("实测定子最热相分布 · 297万格",HL,BB+6);
g.fillStyle=V("--warn");g.font='600 9px "IBM Plex Mono",monospace';
g.fillText("0.0015% 越 130.2°C",HL+2,yv(P0.warn)+3);
g.fillStyle=V("--bad");g.fillText("实测峰值 152.0°C",HL+2,yv(152.0)-11)}
function drawMargin(g){const w=g._w,h2=g._h,P0=TH.params;
g.clearRect(0,0,w,h2);
const L=40,R=w-90,T=18,B2=h2-30, pt=TH.per_turbine_p99, bw=(R-L)/pt.length;
const yv=v=>B2-(v-95)/(160-95)*(B2-T);
[[155,"--bad","绕组保护"],[100,"--warn","风机2级"]].forEach(([v,c,lb])=>{
g.strokeStyle=V(c);g.setLineDash([4,3]);g.lineWidth=1.2;
g.beginPath();g.moveTo(L,yv(v));g.lineTo(R,yv(v));g.stroke();g.setLineDash([]);
g.fillStyle=V(c);g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="left";g.textBaseline="middle";
g.fillText(lb+" "+v,R+4,yv(v))});
[100,110,120,130,140,150].forEach(v=>{g.strokeStyle=V("--line2");g.lineWidth=1;
g.beginPath();g.moveTo(L,yv(v));g.lineTo(R,yv(v));g.stroke();
g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Mono",monospace';g.textAlign="right";g.textBaseline="middle";
g.fillText(v,L-4,yv(v))});
pt.forEach(([tid,v],i)=>{const x=L+bw*i;
g.fillStyle=i===0?V("--warn"):V("--meas");g.globalAlpha=i===0?0.95:0.6;
g.fillRect(x+0.8,yv(v),bw-1.6,B2-yv(v));g.globalAlpha=1});
g.fillStyle=V("--warn");g.font='600 9.5px "IBM Plex Mono",monospace';g.textAlign="left";g.textBaseline="bottom";
g.fillText(pt[0][0]+" "+pt[0][1]+"°C",L+2,yv(pt[0][1])-3);
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Sans Condensed",sans-serif';g.textBaseline="top";
g.fillText("38 台满载定子 p99(热→凉)· 跨台仅差 12.7°C",L,B2+6);
// 降额证据条
const DL=L,DT=T,DR=R;
g.fillStyle=V("--ok");g.font='600 9.5px "IBM Plex Mono",monospace';g.textAlign="right";g.textBaseline="top";
g.fillText("额定以上风·未限电: 每个温度区间功率中位均 4001 kW",DR,DT)}
function drawRail(g){const w=g._w,h2=g._h;
g.clearRect(0,0,w,h2);
const RY=h2*0.52, L=30, R=w*0.60;
// 共轨主管
g.strokeStyle=V("--ink2");g.lineWidth=4;g.lineCap="round";
g.beginPath();g.moveTo(L,RY);g.lineTo(R,RY);g.stroke();
// 泵
const on=S.rload;
g.fillStyle=on?V("--ok"):V("--panel");g.strokeStyle=on?V("--ok"):V("--ink3");g.lineWidth=2;
g.beginPath();g.arc(L,RY,11,0,7);g.fill();g.stroke();
g.fillStyle=on?V("--panel"):V("--ink3");g.font='700 9px "IBM Plex Mono",monospace';
g.textAlign="center";g.textBaseline="middle";g.fillText("P",L,RY);
g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Sans Condensed",sans-serif';
g.fillText(on?"加载":"卸荷",L,RY+22);
// 三个取压点 (向上)
["A","B","C"].forEach((nm,i)=>{const x=L+50+i*((R-L-70)/2.6);
g.strokeStyle=V("--meas");g.lineWidth=2;
g.beginPath();g.moveTo(x,RY);g.lineTo(x,RY-30);g.stroke();
g.fillStyle=V("--measf");g.strokeStyle=V("--meas");g.lineWidth=1.5;
g.beginPath();g.arc(x,RY-38,8,0,7);g.fill();g.stroke();
g.fillStyle=V("--meas");g.font='600 9px "IBM Plex Mono",monospace';g.textAlign="center";g.textBaseline="middle";
g.fillText(nm,x,RY-38);
g.fillStyle=V("--ink");g.font='500 9.5px "IBM Plex Mono",monospace';
g.fillText(S.bl[i],x,RY-54)});
g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Sans Condensed",sans-serif';g.textAlign="center";
g.fillText("三个取压点 · 同一节点",L+(R-L)/2,RY-70);
// 蓄能器 70 号 (近端, 全额参与)
g.strokeStyle=V("--spec");g.lineWidth=2;
g.beginPath();g.moveTo(L+34,RY);g.lineTo(L+34,RY+26);g.stroke();
g.fillStyle=V("--specf");g.strokeStyle=V("--spec");
g.beginPath();g.roundRect(L+24,RY+26,20,26,4);g.fill();g.stroke();
g.fillStyle=V("--spec");g.font='500 8.5px "IBM Plex Mono",monospace';g.textAlign="center";g.textBaseline="top";
g.fillText("70号",L+34,RY+56);g.fillText("10L",L+34,RY+66);
// 长管 + 旋转接头 + 118 单向阀 → 轮毂 106 (弱耦合)
const HX=R-14;
g.strokeStyle=V("--spec");g.setLineDash([5,4]);g.lineWidth=2;
g.beginPath();g.moveTo(HX,RY);g.lineTo(HX,RY+30);g.stroke();g.setLineDash([]);
g.fillStyle=V("--specf");g.beginPath();g.roundRect(HX-16,RY+30,32,34,4);g.fill();g.stroke();
g.fillStyle=V("--spec");g.textAlign="center";g.textBaseline="top";
g.fillText("106号",HX,RY+68);g.fillText("6×30L",HX,RY+78);
g.fillStyle=V("--ink3");g.font='500 8.5px "IBM Plex Sans Condensed",sans-serif';
g.fillText("弱耦合 kH=0.095",HX,RY+10);
// 右: 共轨锯齿
const PL=w*0.66,PR=w-12,PT=18,PB=h2-30, yv=v=>PB-(v-233)/(248-233)*(PB-PT);
g.strokeStyle=V("--line2");g.lineWidth=1;g.fillStyle=V("--ink3");g.font='500 9px "IBM Plex Mono",monospace';
[236,240,245].forEach(v=>{g.beginPath();g.moveTo(PL,yv(v));g.lineTo(PR,yv(v));g.stroke();
g.textAlign="right";g.textBaseline="middle";g.fillText(v,PL-4,yv(v))});
g.strokeStyle=V("--spec");g.setLineDash([4,3]);g.lineWidth=1.2;
[245,236].forEach(v=>{g.beginPath();g.moveTo(PL,yv(v));g.lineTo(PR,yv(v));g.stroke()});g.setLineDash([]);
g.fillStyle=V("--ink3");g.font='500 8.5px "IBM Plex Sans Condensed",sans-serif';g.textAlign="right";
g.fillText("195 = 最低工作门限(远在带外)",PR,PB+2);
g.strokeStyle=V("--meas");g.lineWidth=1.8;g.beginPath();
S.rh.forEach((v,i)=>{const x=PL+i/199*(PR-PL);i?g.lineTo(x,yv(v)):g.moveTo(x,yv(v))});g.stroke();
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Sans Condensed",sans-serif';g.textAlign="left";g.textBaseline="top";
g.fillText("共轨充放循环 · 窗口约 800 s(本层 1min 不可测,示意)",PL,h2-24)}
function drawTwist(g){const w=g._w,h=g._h,cx=w/2,cy=h/2-6;
g.clearRect(0,0,w,h);
const turns=S.twist/360, mag=Math.abs(turns);
// 缠绕螺旋
const N=Math.max(1,Math.ceil(mag*120)), r0=26, dr=(w/2-30-r0)/3;
g.lineWidth=3.2;g.lineCap="round";
g.strokeStyle=mag>=0.97?V("--bad"):(mag>0.6?V("--warn"):V("--spec"));
g.beginPath();
for(let i=0;i<=N;i++){const t=i/120*Math.sign(turns||1), a=t*2*Math.PI-Math.PI/2, r=r0+Math.abs(t)*dr;
const x=cx+Math.cos(a)*r,y=cy+Math.sin(a)*r; i?g.lineTo(x,y):g.moveTo(x,y)}
g.stroke();
// 触发环
[[0.97,V("--meas"),"实测触发 0.97 圈"],[2.08,V("--bad"),"硬限位 2.08 圈"]].forEach(([t,c,lb])=>{
g.strokeStyle=c;g.setLineDash([3,4]);g.lineWidth=1.2;
g.beginPath();g.arc(cx,cy,r0+t*dr,0,7);g.stroke();g.setLineDash([])});
g.fillStyle=V("--ink");g.font='600 19px "IBM Plex Mono",monospace';g.textAlign="center";g.textBaseline="middle";
g.fillText((turns>=0?"+":"")+turns.toFixed(2),cx,cy);
g.fillStyle=V("--ink3");g.font='500 9.5px "IBM Plex Sans Condensed",sans-serif';
g.fillText("圈",cx,cy+15);
g.fillStyle=S.unwing?V("--bad"):V("--ink3");g.font='600 10px "IBM Plex Mono",monospace';
g.fillText(S.unwing?"UNTWISTING":"解缆 "+S.unw+" 次",cx,h-8)}
/* ---------- bind ---------- */
const F={};
function readout(){
const err=wrap(S.wdf-S.nac), sp=D.blade.spread.map(x=>x[1]);
Object.assign(F,{
db:P.deadband.toFixed(1)+"°",dly:P.delay_s.toFixed(1)+" s",spd:P.yaw_spd_pct+"%",tol:P.stop_tol+"° + "+P.stop_off+"°",
err:err.toFixed(2)+"°",nac:S.nac.toFixed(1)+"°",brk:S.brk.toFixed(0)+" bar",
st:S.unwing?"解缆中":(S.yawing?"偏航中":(S.dly>0?"延时 "+S.dly.toFixed(1)+"s":"保持")),
cv:"15 点 · 4.5→17.4 rpm",rpm:S.rpm.toFixed(2)+" rpm",kw:S.kw.toFixed(0)+" kW",pit:S.pitch.toFixed(1)+"°",
dev:"−3.3 ~ −4.2%",dev2:"+59% / −20%",
tw:P.tower_hz+" Hz",n3:NOTCH_C.toFixed(2)+" rpm",nb:NB[0].toFixed(2)+"–"+NB[1].toFixed(2)+" rpm",
tsr:(1.175*S.ws).toFixed(2)+" rpm",rpm3:S.rpm.toFixed(2)+" rpm",ratio:"0.06(深 30×)",
pp:P.pump_p+" bar",ph:"± "+P.pump_hyst+" bar",bl2:P.brk_lo+" / "+P.brk_hi+" bar",
pcur:S.pump.toFixed(1)+" bar",pst:S.pumpOn?"充压中":"泄漏中",pband:"98.1%",plo:"0.04% ← 证伪",
mdl:"A + B·(P/Pn)²",A:TH.A_noload+" °C",Bc:TH.B_copper+" °C",htr:TH.params.heater_dT+" °C",
tnac:S.Tnac.toFixed(1)+" °C",tst:S.Tst.toFixed(1)+" °C",dtn:(S.Tst-S.Tnac).toFixed(1)+" °C",
blw:S.blow===0?"停":(S.blow===1?"1 级":"2 级"),
marg:(TH.params.trip-S.Tst).toFixed(1)+" °C",
htrt:TH.params.htrt_max+"",rdel:TH.params.red_delay+" s",lims:"155 / 100 °C",
a80:TH.above["80"]+"%",a84:TH.above["130.2"]+"%",a100:TH.above["100"]+"%",
pk:TH.stator_max+" °C / 余量 "+(155-TH.stator_max).toFixed(1)+" °C",
dr:"每个温度区间中位均 4001 kW",
rl:"卸荷 245 / 加载 236 bar",ac1:"10 L @ 180 bar(近端,全额)",ac2:"6×30 L @ 125 bar(顺桨主源)",
kh:"0.095(弱耦合)",rail:S.rail.toFixed(1)+" bar",
abc:S.bl.join(" / ")+" bar",spr:(Math.max(...S.bl)-Math.min(...S.bl))+" bar",
corr:"0.948 / 0.952 / 0.953",exc:"16–20 bar / 167 bar",
fy:"789 s/圈",hl:"2.08 圈(跨台中位)",tw2:(S.twist/360).toFixed(2)+" 圈",
trav:Math.abs(S.twist/360).toFixed(2)+" 圈(本次)",um:D.unw_hist.med+" 圈(n="+D.unw_hist.n+")",
uf:"66 次/台 · 677 圈/台"});
document.querySelectorAll("[data-f]").forEach(e=>{const v=F[e.dataset.f];if(v!=null&&e.textContent!==v)e.textContent=v});
}
document.querySelectorAll(".row").forEach(r=>{if(r.querySelector(".k").textContent==="")r.replaceWith(mk("hr","sep"))});
let last=0,paused=0;
function loop(ts){
const raw=(ts-last)/1000; last=ts;
const dt=Math.max(0,Math.min(raw,0.25))*(CV("sp",12)); // 钳非负 + 封顶, 防标签页切回时巨步
if(!paused){step(dt);}
PN.forEach(p=>p.d(p.g)); readout();
requestAnimationFrame(loop)}
const ON=(id,f)=>{const e=$("#"+id);if(e)e.oninput=f};
ON("ws",e=>$("#wso").textContent=(+e.target.value).toFixed(1)+" m/s");
ON("tb",e=>$("#tbo").textContent=(+e.target.value).toFixed(1)+"×");
ON("amb",e=>$("#ambo").textContent=e.target.value+" °C");
ON("sp",e=>$("#spo").textContent=e.target.value+"×");
$("#pz").onclick=e=>{paused=!paused;e.target.textContent=paused?"继续":"暂停"};
$("#th").onclick=()=>{const d=document.documentElement,
cur=d.getAttribute("data-theme")||(matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");
d.setAttribute("data-theme",cur==="dark"?"light":"dark")};
// 罗盘可拖拽改风向
if(PN[0]&&PN[0].d===drawYaw)PN[0].g.canvas.addEventListener("pointerdown",function h(ev){
const f=e=>{const r=this.getBoundingClientRect();
S.wd=(Math.atan2(e.clientY-r.top-r.height/2,e.clientX-r.left-r.width/2)*180/Math.PI+90+360)%360};
f(ev);const mv=f.bind(this),up=()=>{removeEventListener("pointermove",mv);removeEventListener("pointerup",up)};
addEventListener("pointermove",mv);addEventListener("pointerup",up)});
if(SF){document.querySelectorAll("[data-sys]").forEach(e=>{if(e.dataset.sys!==SF)e.remove()});
const T={yaw:["偏航系统","对风死区 · 液压泵压 · 解缆循环"],
drv:["传动链与功率系统","转矩–转速控制律 · 塔筒 3P 共振禁带"],
gen:["发电机与热管理","温升模型 · 热裕度与热降额"],
pit:["变桨与轮毂液压系统","单节点共轨 · 一次结论撤回"]}[SF];
$("#h1").textContent="如东 SWT-4.0-130 · "+T[0]; document.title="如东 SWT-4.0-130 · "+T[0];
document.querySelector(".sub").innerHTML="左边是参数表说机器<em>应该</em>怎么动,右边是 38 台机器 553 天<em>实际</em>怎么动。本页:<b>"+T[1]+"</b>。各面板对照控制律演示与运行数据统计。";}
requestAnimationFrame(loop);
</script>
</body>
</html>