Repository navigation
Expand file tree
/
Copy pathfast_analysis.py
More file actions
343 lines (304 loc) · 13.8 KB
/
Copy pathfast_analysis.py
File metadata and controls
343 lines (304 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
"""
fast_analysis.py — OpenFAST/AeroDyn .out 结果分析模块(独立于 QBlade 分析)
============================================================================
基于 fast_io 解析出的 FAST 数据,进行与 QBlade 类似的载荷分析:
1. 风轮载荷分析:推力 / 扭矩 / 功率 / 转速 / TSR(容错缺列)
2. 叶片节点载荷极值(若输出 B*N*Fx/Fy/Fn/Ft 等分布力)
3. 时序数据(降采样,供前端绘图)
容错原则:即使输出文件缺少部分变量,也能继续分析已存在的变量。
"""
from __future__ import annotations
import math
from typing import Dict, List, Optional, Tuple
import numpy as np
from fast_io import FastFileInfo, FASTChannelMapper, load_fast_data
# ============================================================
# 统计工具(与 QBlade 侧一致口径)
# ============================================================
def _stats(vals: np.ndarray) -> Dict[str, float]:
"""极值统计(全量数据)"""
v = np.asarray(vals, dtype=float)
v = v[~np.isnan(v)]
if len(v) == 0:
return {"max": None, "min": None, "mean": None, "std": None, "rms": None}
return {
"max": float(np.max(v)),
"min": float(np.min(v)),
"mean": float(np.mean(v)),
"std": float(np.std(v, ddof=1)) if len(v) > 1 else 0.0,
"rms": float(np.sqrt(np.mean(v ** 2))),
}
def _peak_time(vals: np.ndarray, t: np.ndarray, mode: str = "max") -> Optional[float]:
"""返回最值发生时刻"""
if vals is None or len(vals) == 0 or t is None or len(t) != len(vals):
return None
v = np.asarray(vals, dtype=float)
if len(v) == 0:
return None
idx = int(np.argmax(v)) if mode == "max" else int(np.argmin(v))
return round(float(t[idx]), 4)
# ============================================================
# 主分析入口
# ============================================================
def run_fast_analysis(filepath: str, info: Optional[FastFileInfo] = None,
max_pts: int = 1200) -> Dict:
"""运行 FAST .out 文件分析,返回 JSON 友好结果字典。
参数:
filepath: .out 文件路径
info: 已解析的 FastFileInfo(None=自动)
max_pts: 时序降采样点数上限
返回: results dict
"""
if info is None:
info = parse_fast_header_local(filepath)
df = load_fast_data(filepath, header=info)
if df.empty:
return {"error": f"文件无有效数据: {filepath}", "file_info": _file_info(info)}
mapper = FASTChannelMapper(info.columns, info.num_blades, info.num_nodes)
t = df["Time"].to_numpy(dtype=float) if "Time" in df.columns else None
results = {
"file_info": _file_info(info),
"overview": {},
"rotor": {},
"blades": {},
"timeseries": {},
"channels": mapper.global_channels,
"missing": [], # 期望输出但文件中缺失的变量(提示用户补输出)
}
# ---- 功率 ----
if "Momentary_Aerodynamic_Power_[W]" in mapper.channels:
col = mapper.channels["Momentary_Aerodynamic_Power_[W]"]
if col in df.columns:
v = df[col].to_numpy(dtype=float)
results["rotor"]["power_stats"] = _stats(v)
results["rotor"]["power_max_time"] = _peak_time(v, t)
results["rotor"]["power_min_time"] = _peak_time(v, t, "min")
results["overview"]["power_mean_kw"] = round(float(np.nanmean(v)) / 1000, 2)
# ---- 转速 / TSR ----
for sys_name, key in [("Rotational_Speed_[rpm]", "rpm_stats"),
("Tip_Speed_Ratio_[-]", "tsr_stats")]:
if sys_name in mapper.channels:
col = mapper.channels[sys_name]
if col in df.columns:
v = df[col].to_numpy(dtype=float)
results["rotor"][key] = _stats(v)
# ---- 推力(水平合力优先,其次 z 分量;均可缺省跳过)----
thrust_cols = mapper.get_horiz_force_cols()
thrust_vec = None
if thrust_cols:
fx, fy = thrust_cols
if fx in df.columns and fy in df.columns:
fxv = df[fx].to_numpy(dtype=float)
fyv = df[fy].to_numpy(dtype=float)
thrust_vec = np.hypot(fxv, fyv)
thrust_src = "sqrt(Fxh^2+Fyh^2)"
if thrust_vec is None:
tcol = mapper.get_thrust_col()
if tcol and tcol in df.columns:
thrust_vec = df[tcol].to_numpy(dtype=float)
thrust_src = tcol
if thrust_vec is not None:
results["rotor"]["thrust_stats"] = _stats(thrust_vec)
results["rotor"]["thrust_max_time"] = _peak_time(thrust_vec, t)
results["rotor"]["thrust_min_time"] = _peak_time(thrust_vec, t, "min")
results["rotor"]["thrust_source"] = thrust_src
results["overview"]["thrust_mean"] = round(float(np.nanmean(thrust_vec)), 1)
# ---- 扭矩(绕旋转轴力矩优先;无则用 P/ω 计算)----
torque_vec = None
torque_src = None
mz_col = mapper.channels.get("Momentary_Aerodynamic_Torque_[Nm]")
if mz_col and mz_col in df.columns:
torque_vec = df[mz_col].to_numpy(dtype=float)
torque_src = mz_col
if torque_vec is None:
# 尝试 P / ω
pcol = mapper.channels.get("Momentary_Aerodynamic_Power_[W]")
rcol = mapper.channels.get("Rotational_Speed_[rpm]")
if pcol in df.columns and rcol in df.columns:
p = df[pcol].to_numpy(dtype=float)
rpm = df[rcol].to_numpy(dtype=float)
omega = np.abs(rpm) * 2.0 * np.pi / 60.0
with np.errstate(divide="ignore", invalid="ignore"):
torque_vec = np.where(omega > 1e-6, p / np.where(omega > 1e-6, omega, 1.0), np.nan)
torque_src = "P / omega"
if torque_vec is not None:
tv = np.asarray(torque_vec, dtype=float)
results["rotor"]["torque_stats"] = _stats(tv)
results["rotor"]["torque_max_time"] = _peak_time(tv, t)
results["rotor"]["torque_min_time"] = _peak_time(tv, t, "min")
results["rotor"]["torque_source"] = torque_src
results["overview"]["torque_mean"] = round(float(np.nanmean(tv)), 1)
# ---- 叶片节点载荷(若输出 B*N*Fx/Fy/Fn/Ft 等)----
results["blades"] = _analyze_blade_nodes(df, mapper)
# ---- 缺失提示:期望输出但未输出的 rotor/叶片变量 ----
_fill_missing(df, mapper, results)
# ---- 时序(降采样,供前端画图)----
results["timeseries"] = _make_timeseries(df, mapper, max_pts)
return results
def _analyze_blade_nodes(df, mapper: FASTChannelMapper) -> Dict:
"""叶片节点载荷极值分析(容错:无节点载荷列则返回空)"""
blades: Dict[str, Dict] = {}
for key, node in mapper.blade_node_cols.items():
# key 形如 Blade_1_Node_1
parts = key.split("_")
blade_key = f"Blade_{parts[1]}"
node_num = int(parts[3])
if blade_key not in blades:
blades[blade_key] = {"nodes": {}, "suffixes_available": []}
node_vals = {}
for suffix, col in node.items():
if col in df.columns:
v = df[col].to_numpy(dtype=float)
node_vals[suffix] = _stats(v)
blades[blade_key]["suffixes_available"].append(suffix)
if node_vals:
blades[blade_key]["nodes"][str(node_num)] = node_vals
# 去重 suffixes
for b in blades.values():
b["suffixes_available"] = sorted(set(b["suffixes_available"]))
return blades
def _fill_missing(df, mapper: FASTChannelMapper, results: Dict):
"""提示期望输出但缺失的关键变量(帮助用户修正 OutList 输出设置)"""
wanted = ["RtAeroFxh", "RtAeroFyh", "RtAeroFzh",
"RtAeroMxh", "RtAeroMyh", "RtAeroMzh",
"RtAeroPwr", "RtSpeed", "RtTSR"]
missing = []
for w in wanted:
if not mapper._norm.get(w) and w not in df.columns:
missing.append(w)
# 叶片载荷:任一叶片任一节点 Fx/Fy 缺失即提示
blade_sample = ["B1N1Fx", "B1N1Fy", "B1N1Fn", "B1N1Ft", "B1N1Mm"]
for w in blade_sample:
if not mapper._norm.get(w) and w not in df.columns:
missing.append(w)
results["missing"] = missing
def _make_timeseries(df, mapper: FASTChannelMapper, max_pts: int) -> Dict:
"""降采样时序,供前端绘图(含 rotor 载荷通道)"""
t = df["Time"].to_numpy(dtype=float)
n = len(t)
step = max(1, n // max_pts)
# 采样索引含首尾
idx = list(range(0, n, step))
if idx[-1] != n - 1:
idx.append(n - 1)
ch: Dict[str, List[float]] = {}
# 优先输出系统通道
sys_map = {
"Momentary_Aerodynamic_Thrust_[N]": "thrust",
"Momentary_Aerodynamic_Torque_[Nm]": "torque",
"Momentary_Aerodynamic_Power_[W]": "power",
"Rotational_Speed_[rpm]": "rpm",
"Tip_Speed_Ratio_[-]": "tsr",
"Abs_Meas._Wind_Vel._at_Hub_[m/s]": "wind_speed",
}
# 推力向量(组合水平合力优先)
thrust_vec = None
if mapper.get_horiz_force_cols():
fx, fy = mapper.get_horiz_force_cols()
if fx in df.columns and fy in df.columns:
thrust_vec = np.hypot(df[fx].to_numpy(dtype=float), df[fy].to_numpy(dtype=float))
if thrust_vec is None:
tcol = mapper.get_thrust_col()
if tcol and tcol in df.columns:
thrust_vec = df[tcol].to_numpy(dtype=float)
# 扭矩向量(绕轴力矩优先,其次 P/ω)
torque_vec = None
mz_col = mapper.channels.get("Momentary_Aerodynamic_Torque_[Nm]")
if mz_col and mz_col in df.columns:
torque_vec = df[mz_col].to_numpy(dtype=float)
if torque_vec is None:
pcol = mapper.channels.get("Momentary_Aerodynamic_Power_[W]")
rcol = mapper.channels.get("Rotational_Speed_[rpm]")
if pcol in df.columns and rcol in df.columns:
p = df[pcol].to_numpy(dtype=float)
rpm = df[rcol].to_numpy(dtype=float)
omega = np.abs(rpm) * 2.0 * np.pi / 60.0
with np.errstate(divide="ignore", invalid="ignore"):
torque_vec = np.where(omega > 1e-6, p / np.where(omega > 1e-6, omega, 1.0), np.nan)
for sys_name, key in sys_map.items():
if key == "thrust":
if thrust_vec is not None:
ch[key] = [round(float(x), 3) for x in thrust_vec[idx]]
continue
if key == "torque":
if torque_vec is not None:
ch[key] = [round(float(x), 3) for x in torque_vec[idx]]
continue
col = mapper.channels.get(sys_name)
if col and col in df.columns:
v = df[col].to_numpy(dtype=float)
ch[key] = [round(float(x), 3) for x in v[idx]]
return {"time": [round(float(x), 3) for x in t[idx]], "channels": ch}
def _file_info(info: FastFileInfo) -> Dict:
return {
"filename": info.filename,
"size_mb": round(info.file_size_mb, 2),
"num_rows": info.num_rows,
"num_cols": info.num_columns,
"num_blades": info.num_blades,
"num_nodes": info.num_nodes,
"sample_rate": round(info.sample_rate_hz, 1),
"duration": round(info.total_time, 2),
"generator": info.generator,
"condition_type": "FAST",
}
def parse_fast_header_local(filepath: str) -> FastFileInfo:
"""本地解析(避免循环依赖)"""
from fast_io import parse_fast_header
return parse_fast_header(filepath)
# ============================================================
# 通道向量计算(供前端时序接口复用)
# ============================================================
FAST_CHANNEL_LABELS = {
"thrust": "风轮推力",
"torque": "风轮扭矩",
"power": "风轮功率",
"rpm": "转速",
"tsr": "叶尖速比",
"wind_speed": "风速",
}
def fast_channel_vector(df, mapper: FASTChannelMapper, key: str):
"""返回 FAST 通道全量向量 (values: np.ndarray, label: str)
key 可取:
- 特殊键 thrust/torque/power/rpm/tsr/wind_speed(自动计算/组合)
- 或 df 原始列名(直接返回该列)
若通道不可用返回 (None, label)。
"""
if key == "thrust":
if mapper.get_horiz_force_cols():
fx, fy = mapper.get_horiz_force_cols()
if fx in df.columns and fy in df.columns:
return np.hypot(df[fx].to_numpy(dtype=float), df[fy].to_numpy(dtype=float)), "风轮推力"
tcol = mapper.get_thrust_col()
if tcol and tcol in df.columns:
return df[tcol].to_numpy(dtype=float), "风轮推力"
return None, "风轮推力"
if key == "torque":
mz = mapper.channels.get("Momentary_Aerodynamic_Torque_[Nm]")
if mz and mz in df.columns:
return df[mz].to_numpy(dtype=float), "风轮扭矩"
pcol = mapper.channels.get("Momentary_Aerodynamic_Power_[W]")
rcol = mapper.channels.get("Rotational_Speed_[rpm]")
if pcol in df.columns and rcol in df.columns:
p = df[pcol].to_numpy(dtype=float)
rpm = df[rcol].to_numpy(dtype=float)
omega = np.abs(rpm) * 2.0 * np.pi / 60.0
with np.errstate(divide="ignore", invalid="ignore"):
return np.where(omega > 1e-6, p / np.where(omega > 1e-6, omega, 1.0), np.nan), "风轮扭矩"
return None, "风轮扭矩"
sys_map = {
"power": ("Momentary_Aerodynamic_Power_[W]", "风轮功率"),
"rpm": ("Rotational_Speed_[rpm]", "转速"),
"tsr": ("Tip_Speed_Ratio_[-]", "叶尖速比"),
"wind_speed": ("Abs_Meas._Wind_Vel._at_Hub_[m/s]", "风速"),
}
if key in sys_map:
sys_name, label = sys_map[key]
col = mapper.channels.get(sys_name)
if col and col in df.columns:
return df[col].to_numpy(dtype=float), label
return None, label
# 原始列名
if key in df.columns:
return df[key].to_numpy(dtype=float), key
return None, key