|
|
@@ -597,3 +597,139 @@ def fleet(win: str = '2026年') -> dict[str, Any]:
|
|
|
`TypeError: fleet_view() missing 1 required positional argument: 'win'`(算法服务 500)。
|
|
|
"""
|
|
|
return fleet_view(win)
|
|
|
+from src import paths as _P # 路径唯一真源 (与 cwd 无关)
|
|
|
+from src.windscada import taxonomy
|
|
|
+from src.windscada.subsys import temp_nbm, hydraulic, yaw as yawmod, pitch as pitchmod
|
|
|
+import hashlib, time
|
|
|
+import numpy as np, pandas as pd
|
|
|
+import threading as _thr
|
|
|
+
|
|
|
+# ── curves 视图所需(同法逐字搬) ──
|
|
|
+LENSES = [('wsb', 'grd_wtc_ActPower_mean', 'L1 功率曲线 (风速-功率)', '风速 m/s (机舱风)', 'kW'),
|
|
|
+ ('wsb', 'cp', 'L6 风能利用系数 Cp (风速-Cp)', '风速 m/s', 'Cp'),
|
|
|
+ ('wsb', 'lam', 'L5 叶尖速比 λ (风速-λ)', '风速 m/s', 'λ'),
|
|
|
+ ('grb', 'tq', 'L4 转矩-转速 (发电机侧)', '发电机转速 rpm', 'kNm'),
|
|
|
+ ('pwb', 'tur_wtc_PitcPosA_mean', 'L2 功率-桨距 (控制律/削峰)', '功率 kW', '°'),
|
|
|
+ ('pwb', 'tur_wtc_GenRpm_mean', 'L3 功率-发电机转速 (饱和点)', '功率 kW', 'rpm'),
|
|
|
+ ('pwb', 'p3', 'L2b 三叶桨距极差 (集距/不平衡)', '功率 kW', '°'),
|
|
|
+ ('pwb', 'ratio', 'L7 转速比 gen/rot (传动链)', '功率 kW', '—')]
|
|
|
+
|
|
|
+CFG = farm(); ST = pathlib.Path(CFG['store'])
|
|
|
+
|
|
|
+_WIN_BUSY: dict = {'sysmx': set(), 'curves': set(), 'm9': set()}
|
|
|
+
|
|
|
+_WIN_CACHE: dict = {'sysmx': {}, 'curves': {}, 'm9': {}}
|
|
|
+
|
|
|
+_WIN_ERR: dict = {'sysmx': {}, 'curves': {}, 'm9': {}}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+def curves_of(win):
|
|
|
+ """→ (七镜头分箱件 或 None, pending)。按窗重算(干净窗就是正式产物本身)。"""
|
|
|
+ from src.windscada.perf import curves as _cv
|
|
|
+ a, b = span_of(win)
|
|
|
+ if (a, b) == (_cv.WIN[0], '2025-12-31'): # 与正式产物的判别窗一致 ⇒ 直接用产物, 不重算
|
|
|
+ import pandas as _pd
|
|
|
+ return _pd.read_parquet(ST / 'curve_lenses.parquet'), False
|
|
|
+ st = _win_get('curves', win, lambda: _cv.build_store(CFG, span=(a, b), write=False))
|
|
|
+ return st, st is None
|
|
|
+
|
|
|
+
|
|
|
+def curve_view(win='2025H2'):
|
|
|
+ """特性曲线多镜头 (SOP §4.6c): fleet 中位+四分位带 + 过绝对锚的离群台曲线.
|
|
|
+
|
|
|
+ ★2026-09-21 用户令「发电性能随时间窗变化」: 七镜头**按所选时间窗重算**(首次后台算,先回 pending),
|
|
|
+ 7 张月度时序图按所选窗的月份过滤。原口径(2025H2 干净判别窗)仍可作为窗之一被选中,
|
|
|
+ 选到它时直接用正式产物,不重算。
|
|
|
+ """
|
|
|
+ import pandas as pd
|
|
|
+ from src.windscada.perf import curves as cv
|
|
|
+ a, b = span_of(win)
|
|
|
+ ms = months_of(win)
|
|
|
+ store, pending = curves_of(win)
|
|
|
+ if pending:
|
|
|
+ return dict(building=True, win=win, span=[a, b], months=ms,
|
|
|
+ note=f'正在按所选时间窗 {a} ~ {b} 重算七镜头曲线(首次约 10~45 秒),完成后自动刷新。'
|
|
|
+ f'期间下方月度时序图已按所选窗过滤,可直接看。')
|
|
|
+ wlabel = f'{a} ~ {b}'
|
|
|
+ figs = []
|
|
|
+ for xc, yc, title, xlab, unit in LENSES:
|
|
|
+ r = cv.lens(xc, yc, store_df=store, win_label=wlabel)
|
|
|
+ if not r: continue
|
|
|
+ outs = sorted(r['离群'].items(), key=lambda kv: -abs(kv[1]['z']))[:3]
|
|
|
+ a = r['anchor']
|
|
|
+ series = [dict(name='全场中位', vals=r['fleet'])]
|
|
|
+ series += [dict(name=f"{t.replace('WTG','')}# {v['resid']:+.4g}{a['单位']}", vals=r['per_t'][t]) for t, v in outs]
|
|
|
+ figs.append(dict(kind='multiline', title=title, unit=unit, months=[f"{x:g}" for x in r['x']],
|
|
|
+ series=series, xlab=xlab,
|
|
|
+ note=(f"显著门 |残差|≥{a['门']}{a['单位']} ({a['说明']}) ∧ |z|≥3 → 离群 "
|
|
|
+ f"{'/'.join(t.replace('WTG','')+'#' for t,_ in outs) if outs else '无'}"
|
|
|
+ f" | 灰域=全场四分位, 逐档中位聚合, 已剥限电(只正常发电态)"
|
|
|
+ f" | 样本 {r['n_total']:,} 个10min点 / {r['n_turbines']} 台 / 最小档 {r['n_min_bin']:,}"
|
|
|
+ f" | 时间窗 {r['win']}(随所选时间窗)"),
|
|
|
+ band=dict(q1=r['q1'], q3=r['q3'])))
|
|
|
+ # 残差视图 (§4.6c④ per机必算同型残差): 绝对量尺度上四分位带只有几个像素, 残差面才看得见
|
|
|
+ if outs:
|
|
|
+ rs = [dict(name=f"{t.replace('WTG','')}# {v['resid']:+.4g}{a['单位']}",
|
|
|
+ vals=[None if (r['per_t'][t][i] is None or r['fleet'][i] is None) else round(r['per_t'][t][i] - r['fleet'][i], 4)
|
|
|
+ for i in range(len(r['x']))]) for t, v in outs]
|
|
|
+ q1d = [round(r['q1'][i] - r['fleet'][i], 4) for i in range(len(r['x']))]
|
|
|
+ q3d = [round(r['q3'][i] - r['fleet'][i], 4) for i in range(len(r['x']))]
|
|
|
+ figs.append(dict(kind='multiline', title=title.split(' (')[0] + ' · 同型机群残差 (本台−全场中位)', unit=unit,
|
|
|
+ months=[f"{x:g}" for x in r['x']], series=rs, xlab=xlab,
|
|
|
+ thresholds=[dict(v=a['门'], label=f"显著门 +{a['门']}{a['单位']}"), dict(v=-a['门'], label='')],
|
|
|
+ band=dict(q1=q1d, q3=q3d),
|
|
|
+ note=(f"虚线=物理绝对锚 ±{a['门']}{a['单位']} ({a['说明']}); 灰域=全场四分位残差带; 出带且过锚才算离群"
|
|
|
+ f" | 样本 {r['n_total']:,} 点 / {r['n_turbines']} 台 | 时间窗 {r['win']}")))
|
|
|
+ # ---- 时序件: 控制参数月度 (M9b) + 偏航动态月度 (M12) ----
|
|
|
+ def _ts(path, val, title, unit, xlab, picks=None, note='', thr=None):
|
|
|
+ f = ST / path
|
|
|
+ if not f.exists(): return None
|
|
|
+ d = pd.read_parquet(f)
|
|
|
+ if val not in d.columns: return None
|
|
|
+ piv = d.pivot_table(index='month', columns='turbine', values=val)
|
|
|
+ # ★2026-09-21 用户令: 月度时序图按**所选时间窗**的月份过滤(月度件无法按日切,如实写"按月取整")
|
|
|
+ if ms:
|
|
|
+ piv = piv.reindex([m for m in [str(x) for x in piv.index] if m in set(ms)])
|
|
|
+ if not len(piv): return None
|
|
|
+ msx = [str(x) for x in piv.index]
|
|
|
+ med = piv.median(axis=1)
|
|
|
+ dev = (piv.sub(med, axis=0)).abs().mean()
|
|
|
+ picks = picks or list(dev.sort_values(ascending=False).head(3).index)
|
|
|
+ series = [dict(name='全场中位', vals=[None if v != v else round(float(v), 3) for v in med])]
|
|
|
+ series += [dict(name=t.replace('WTG', '') + '#', vals=[None if v != v else round(float(v), 3) for v in piv[t]])
|
|
|
+ for t in picks if t in piv.columns]
|
|
|
+ # 样本量与窗随图走 (2026-08-28 门禁 F1): 7 张时序图原来只有结论没有依据规模,
|
|
|
+ # 违反第一性原理②"Sample size declared"。逐月×台的非空格数即样本量。
|
|
|
+ _n = int(piv.notna().to_numpy().sum())
|
|
|
+ _nt = int(piv.shape[1])
|
|
|
+ note2 = (note + f" | 样本 {_n:,} 个月×台 / {_nt} 台 / {len(msx)} 个月"
|
|
|
+ f" | 时间窗 {msx[0]}~{msx[-1]}(月度聚合,随所选时间窗·按月取整)") if msx else note
|
|
|
+ return dict(kind='multiline', title=title, unit=unit, months=msx, series=series, xlab=xlab,
|
|
|
+ note=note2, thresholds=thr or [],
|
|
|
+ band=dict(q1=[None if v != v else round(float(v), 3) for v in piv.quantile(0.25, axis=1)],
|
|
|
+ q3=[None if v != v else round(float(v), 3) for v in piv.quantile(0.75, axis=1)]))
|
|
|
+ for args in (
|
|
|
+ ('control_monthly.parquet', 'p_cap', '时序① 满发功率封顶 月度 (取每月高位稳定值; 阶跃=配置变更)', 'kW', '月', None,
|
|
|
+ '灰域=全场四分位; 参数级分组见发电性能页 (4175/4200/4225 三组); 阶跃=配置变更, 平移=工况'),
|
|
|
+ ('control_monthly.parquet', 'w_cap', '时序② 转速封顶 月度 (取每月高位稳定值)', 'rpm', '月', None, '两轴独立分组 (1672/1680)'),
|
|
|
+ ('control_monthly.parquet', 'pitch_rated', '时序③ 额定段桨距角 月度 (标定漂移)', '°', '月',
|
|
|
+ ['WTG03', 'WTG34', 'WTG19'], '离群台=桨距调度同档差 top3'),
|
|
|
+ ('yaw_dynamic_monthly.parquet', 'err_sd', '时序④ 对风散布 σ 月度 (原始, 未清洗)', '°', '月',
|
|
|
+ ['WTG08', 'WTG16', 'WTG04'], '三台19个月全程高 → 非发作型; 清洗后降至5-7°, 判为机舱位置通道拖偏(A类数据质量)非对风故障'),
|
|
|
+ ('yaw_dynamic_monthly.parquet', 'travel_day', '时序⑤ 偏航行程 月度 (抗采样物理量)', '°/日', '月', None,
|
|
|
+ '行程=活动量代理, 非真磨损量; 与状态位次数互核'),
|
|
|
+ ('yaw_dynamic_monthly.parquet', 'twist_span', '时序⑥ 扭缆角月跨度 (ScYawPos 累计位置)', '°', '月', None, '解缆动作后回零'),
|
|
|
+ ('yaw_dynamic_monthly.parquet', 'pump_med', '时序⑦ 偏航泵压 月度 (小站泵压)', 'bar', '月', None, '全场极齐 → 无泵压异常'),
|
|
|
+ ):
|
|
|
+ f = _ts(*args)
|
|
|
+ if f: figs.append(f)
|
|
|
+ return dict(figs=figs, physics=cv.physics_check(),
|
|
|
+ note=('镜头口径 (SOP §4.6c): X轴优先功率(直测干净量); 机舱风 self-ref → 只判形状/相对, '
|
|
|
+ '绝对达成率须现场测风; per机异常必算同型机群残差(禁眼估); 工况段筛选=剔除0kW与低转速混合态档'))
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+def curves(win: str = '2026年') -> dict[str, Any]:
|
|
|
+ """详情层口径的薄包装(`curve_view(q.get('win') or '2026年')`)。"""
|
|
|
+ return curve_view(win)
|