| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- import pandas as pd
- import numpy as np
- import math
- from pathlib import Path
- import matplotlib.pyplot as plt
- import seaborn as sns
- from typing import Optional, Tuple, Dict, List
- import re
- class AerodynamicThrustCalculator:
- """气动推力系数计算器类 - 按标准机型和描述分组"""
-
- def __init__(self, csv_path: str):
- """
- 初始化计算器
-
- Args:
- csv_path: CSV文件路径
- """
- self.csv_path = csv_path
- self.data = None
- self.results = None
- self.grouped_results = None
- self.figure_size = (12, 8)
- self.total_groups = 0
-
- def load_data(self) -> bool:
- """从CSV文件加载数据"""
- try:
- self.data = pd.read_csv(self.csv_path)
- print(f"成功加载数据,共 {len(self.data)} 条记录")
- print(f"包含 {self.data['标准机型'].nunique()} 种标准机型")
- print(f"包含 {self.data['描述'].nunique()} 种不同的描述")
-
- # 计算总分组数
- self.total_groups = self.data.groupby(['标准机型', '描述']).ngroups
- print(f"共 {self.total_groups} 个分组")
-
- return True
- except FileNotFoundError:
- print(f"错误:找不到文件 {self.csv_path}")
- return False
- except Exception as e:
- print(f"加载数据时发生错误: {e}")
- return False
-
- def validate_data(self) -> bool:
- """验证数据完整性"""
- required_columns = ['标准机型', '描述', '风速', '有功功率', '空气密度']
-
- for col in required_columns:
- if col not in self.data.columns:
- print(f"错误:缺失必要列 '{col}'")
- return False
-
- # 检查数据类型
- try:
- self.data['风速'] = pd.to_numeric(self.data['风速'])
- self.data['有功功率'] = pd.to_numeric(self.data['有功功率'])
- self.data['空气密度'] = pd.to_numeric(self.data['空气密度'])
- except ValueError as e:
- print(f"数据类型转换错误: {e}")
- return False
-
- print("数据验证通过")
- return True
-
- def extract_rotor_diameter_from_model(self, turbine_model: str) -> float:
- """
- 从机型名称中提取叶轮直径
-
- Args:
- turbine_model: 机型名称,如 'G58-850', 'MY2.0se-121'
-
- Returns:
- 叶轮直径(米)
- """
- # 常见机型叶轮直径映射(可根据实际情况扩展)
- diameter_map = {
- # 歌美飒
- 'G58': 58.0, 'G58-850': 58.0,
- # 明阳智能
- 'MY2.0se-121': 121.0,
- 'MY3.0se-145': 145.0,
- 'MY4.0se-166': 166.0,
- 'MY6.25se-172': 172.0,
- # 东方电气
- 'DEW-G5500-183': 183.0,
- # 金风科技
- 'GW82-1500': 82.0, 'GW82/1500': 82.0,
- 'GW140-3300': 140.0, 'GW140/3300': 140.0,
- # 上海电气
- 'W2000-111': 111.0,
- # 远景能源
- 'EN141-2650': 141.0, 'EN-141/2.65': 141.0,
- 'EN182-6250': 182.0,
- # 国电联合动力
- 'CCWE3000-122.HD': 122.0,
- 'CCWE1500-82.DF': 82.0,
- # 维斯塔斯
- 'V52-850': 52.0,
- # 湘电风能
- 'XE72-2000': 72.0,
- 'XE122-2500': 122.0,
- # 华锐风电
- 'SL1500-82': 82.0, 'SL-82/1.5': 82.0,
- # 其他
- 'S48-750': 48.0
- }
-
- # 尝试直接匹配
- if turbine_model in diameter_map:
- return diameter_map[turbine_model]
-
- # 尝试从名称中提取数字
- try:
- # 匹配两个连字符之间的数字,如 "G58-850" 中的 58
- pattern = r'-(\d+\.?\d*)-'
- match = re.search(pattern, turbine_model)
- if match:
- return float(match.group(1))
-
- # 匹配末尾的数字,如 "MY2.0se-121" 中的 121
- pattern2 = r'-(\d+\.?\d*)$'
- match2 = re.search(pattern2, turbine_model)
- if match2:
- return float(match2.group(1))
-
- # 匹配任意位置的数字
- pattern3 = r'(\d+\.?\d*)'
- matches = re.findall(pattern3, turbine_model)
- if matches:
- # 取最大的数字作为叶轮直径(通常叶轮直径是较大的数字)
- diameters = [float(m) for m in matches]
- # 过滤掉可能的小数字(如2.0中的2)
- filtered_diameters = [d for d in diameters if d > 20]
- if filtered_diameters:
- return max(filtered_diameters)
- elif diameters:
- return max(diameters)
-
- print(f"警告:无法从机型名称 '{turbine_model}' 中提取叶轮直径,使用默认值 80m")
- return 80.0 # 默认值
-
- except Exception as e:
- print(f"提取叶轮直径时出错: {e}")
- return 80.0 # 默认值
-
- def extract_rotor_diameter_from_description(self, description: str) -> Optional[float]:
- """
- 从描述中提取叶轮直径
-
- Args:
- description: 描述文本
-
- Returns:
- 叶轮直径(米),如果无法提取则返回None
- """
- try:
- # 尝试从描述中提取直径信息
- patterns = [
- r'(\d+\.?\d*)[mM]\s*(直径|叶轮|叶片)',
- r'直径\s*(\d+\.?\d*)\s*[mM]',
- r'叶片长度\s*(\d+\.?\d*)\s*[mM]',
- r'风轮直径\s*(\d+\.?\d*)\s*[mM]',
- r'D\s*[=:]\s*(\d+\.?\d*)\s*[mM]'
- ]
-
- for pattern in patterns:
- match = re.search(pattern, description)
- if match:
- return float(match.group(1))
-
- return None
- except Exception as e:
- print(f"从描述中提取叶轮直径时出错: {e}")
- return None
-
- def get_rotor_diameter(self, turbine_model: str, description: str) -> float:
- """
- 获取叶轮直径,优先从描述中提取,然后从机型名称中提取
-
- Args:
- turbine_model: 机型名称
- description: 描述文本
-
- Returns:
- 叶轮直径(米)
- """
- # 首先尝试从描述中提取
- diameter_from_desc = self.extract_rotor_diameter_from_description(description)
- if diameter_from_desc is not None:
- print(f"从描述中提取叶轮直径: {diameter_from_desc}m (机型: {turbine_model})")
- return diameter_from_desc
-
- # 如果无法从描述中提取,则从机型名称中提取
- diameter_from_model = self.extract_rotor_diameter_from_model(turbine_model)
- print(f"从机型名称中提取叶轮直径: {diameter_from_model}m (机型: {turbine_model})")
- return diameter_from_model
-
- def calculate_thrust_coefficient_for_group(self, group_data: pd.DataFrame,
- turbine_model: str, description: str) -> pd.DataFrame:
- """
- 计算单个分组的气动推力系数
-
- Args:
- group_data: 分组数据
- turbine_model: 标准机型
- description: 描述
-
- Returns:
- 包含计算结果的DataFrame
- """
- # 创建副本
- result_df = group_data.copy()
-
- # 获取叶轮直径
- rotor_diameter = self.get_rotor_diameter(turbine_model, description)
-
- # 计算扫风面积 (A = π × (D/2)²)
- swept_area = np.pi * (rotor_diameter / 2) ** 2
-
- # 转换功率单位:kW → W
- power_watts = result_df['有功功率'] * 1000
-
- # 获取其他参数
- air_density = result_df['空气密度']
- wind_speed = result_df['风速']
-
- # 初始化结果列
- result_df['叶轮直径_D_m'] = rotor_diameter
- result_df['扫风面积_A_m2'] = swept_area
- result_df['气动推力_F_N'] = 0.0
- result_df['气动推力系数_Ct'] = 0.0
-
- # 过滤掉风速为0的行(避免除零错误)
- valid_indices = wind_speed > 0
-
- if valid_indices.any():
- # 计算气动推力: F = 2 * ρ * A * P / v
- F = (2 * air_density[valid_indices] * swept_area *
- power_watts[valid_indices] / wind_speed[valid_indices])
-
- # 计算气动推力系数: Ct = (2 × F) / (ρ * v² * A)
- Ct = (2 * F) / (air_density[valid_indices] *
- wind_speed[valid_indices] ** 2 * swept_area)
-
- # 赋值回结果DataFrame
- result_df.loc[valid_indices, '气动推力_F_N'] = F
- result_df.loc[valid_indices, '气动推力系数_Ct'] = Ct
-
- # 风速为0时,推力系数设为0
- zero_indices = wind_speed == 0
- if zero_indices.any():
- result_df.loc[zero_indices, '气动推力系数_Ct'] = 0.0
-
- # 添加分组标识(缩短描述以避免文件名过长)
- short_desc = description[:50].replace('/', '_').replace('\\', '_').replace(':', '_')
- short_desc = re.sub(r'[<>:"|?*]', '_', short_desc) # 移除Windows文件名非法字符
- result_df['分组标识'] = f"{turbine_model}_{short_desc}"
-
- return result_df
-
- def calculate_thrust_coefficient(self) -> bool:
- """按标准机型和描述分组计算气动推力系数"""
- if self.data is None:
- print("错误:请先加载数据")
- return False
-
- # 按标准机型和描述分组
- grouped_data = self.data.groupby(['标准机型', '描述'])
- all_results = []
-
- print(f"\n开始计算 {self.total_groups} 个分组的气动推力系数...")
-
- for (turbine_model, description), group in grouped_data:
- print(f" 计算: 机型={turbine_model}, 描述={description[:50]}... ({len(group)} 条数据)")
-
- # 计算当前分组
- group_result = self.calculate_thrust_coefficient_for_group(
- group, turbine_model, description
- )
- all_results.append(group_result)
-
- # 合并所有结果
- if all_results:
- self.results = pd.concat(all_results, ignore_index=True)
- self.grouped_results = self.results.copy()
- print(f"\n计算完成!共计算 {self.total_groups} 个分组")
- return True
- else:
- print("错误:没有数据可用于计算")
- return False
-
- def save_results(self, output_path: str = None) -> bool:
- """保存计算结果到CSV文件"""
- if self.results is None:
- print("错误:请先计算气动推力系数")
- return False
-
- if output_path is None:
- # 生成默认输出路径
- original_path = Path(self.csv_path)
- output_path = original_path.parent / f"{original_path.stem}_计算结果.csv"
-
- try:
- # 选择要保存的列(保持原有列)
- save_columns = list(self.results.columns)
-
- self.results.to_csv(output_path, index=False, encoding='utf-8-sig')
- print(f"完整结果已保存到: {output_path}")
-
- # 同时保存一个简化版本
- simplified_path = Path(output_path).parent / f"{Path(output_path).stem}_简化版.csv"
- simplified_cols = [
- '风场id', '风场名称', '标准机型', '原始机型', '风速',
- '有功功率', '空气密度', '叶轮直径_D_m', '扫风面积_A_m2',
- '气动推力_F_N', '气动推力系数_Ct', '分组标识', '描述'
- ]
- existing_cols = [col for col in simplified_cols if col in self.results.columns]
- self.results[existing_cols].to_csv(simplified_path, index=False, encoding='utf-8-sig')
- print(f"简化结果已保存到: {simplified_path}")
-
- return True
- except Exception as e:
- print(f"保存结果时发生错误: {e}")
- return False
-
- def save_grouped_summary(self, output_path: str = None) -> bool:
- """保存分组统计摘要到CSV文件"""
- if self.results is None:
- print("错误:请先计算气动推力系数")
- return False
-
- if output_path is None:
- original_path = Path(self.csv_path)
- output_path = original_path.parent / f"{original_path.stem}_分组摘要.csv"
-
- try:
- summary_data = []
-
- # 按分组标识分组
- for group_id, group_data in self.results.groupby('分组标识'):
- # 获取标准机型(从分组标识中提取)
- parts = group_id.split('_')
- turbine_model = parts[0] if parts else ''
-
- # 获取有效数据(风速>0)
- valid_data = group_data[group_data['风速'] > 0]
-
- if len(valid_data) > 0:
- summary = {
- '标准机型': turbine_model,
- '描述': group_data['描述'].iloc[0] if '描述' in group_data.columns else '',
- '分组标识': group_id,
- '数据点数': len(group_data),
- '有效计算点数': len(valid_data),
- '叶轮直径_m': group_data['叶轮直径_D_m'].iloc[0] if '叶轮直径_D_m' in group_data.columns else '未知',
- '扫风面积_m2': group_data['扫风面积_A_m2'].iloc[0] if '扫风面积_A_m2' in group_data.columns else '未知',
- '空气密度_kg_m3': group_data['空气密度'].mean(),
- '风速范围_m_s': f"{group_data['风速'].min():.1f}-{group_data['风速'].max():.1f}",
- '最大推力系数': valid_data['气动推力系数_Ct'].max(),
- '最小推力系数': valid_data['气动推力系数_Ct'].min(),
- '平均推力系数': valid_data['气动推力系数_Ct'].mean(),
- '推力系数标准差': valid_data['气动推力系数_Ct'].std(),
- '最大推力_N': valid_data['气动推力_F_N'].max(),
- '最小推力_N': valid_data['气动推力_F_N'].min(),
- '平均推力_N': valid_data['气动推力_F_N'].mean(),
- '额定功率_kW': group_data['有功功率'].max(),
- '切入风速_m_s': group_data[group_data['有功功率'] > 0]['风速'].min() if len(group_data[group_data['有功功率'] > 0]) > 0 else 0,
- '额定风速_m_s': group_data[group_data['有功功率'] == group_data['有功功率'].max()]['风速'].min() if len(group_data[group_data['有功功率'] == group_data['有功功率'].max()]) > 0 else 0
- }
- summary_data.append(summary)
-
- summary_df = pd.DataFrame(summary_data)
-
- # 按标准机型排序
- if '标准机型' in summary_df.columns:
- summary_df = summary_df.sort_values(['标准机型', '平均推力系数'], ascending=[True, False])
-
- summary_df.to_csv(output_path, index=False, encoding='utf-8-sig')
- print(f"分组摘要已保存到: {output_path}")
-
- # 同时保存每个机型的汇总统计
- if '标准机型' in summary_df.columns:
- model_summary_path = Path(output_path).parent / f"{Path(output_path).stem}_机型汇总.csv"
- model_summary = summary_df.groupby('标准机型').agg({
- '数据点数': 'sum',
- '有效计算点数': 'sum',
- '平均推力系数': 'mean',
- '推力系数标准差': 'mean',
- '平均推力_N': 'mean',
- '叶轮直径_m': 'first'
- }).reset_index()
- model_summary.to_csv(model_summary_path, index=False, encoding='utf-8-sig')
- print(f"机型汇总已保存到: {model_summary_path}")
-
- return True
- except Exception as e:
- print(f"保存分组摘要时发生错误: {e}")
- return False
-
- def save_thrust_curve_data(self, output_path: str = None) -> bool:
- """保存推力系数曲线数据(每个风速点的推力系数)"""
- if self.results is None:
- print("错误:请先计算气动推力系数")
- return False
-
- if output_path is None:
- original_path = Path(self.csv_path)
- output_path = original_path.parent / f"{original_path.stem}_推力曲线数据.csv"
-
- try:
- # 选择关键列
- curve_columns = [
- '分组标识', '标准机型', '描述', '风速',
- '有功功率', '气动推力系数_Ct', '气动推力_F_N',
- '叶轮直径_D_m', '空气密度'
- ]
-
- # 只保留存在的列
- existing_columns = [col for col in curve_columns if col in self.results.columns]
- curve_data = self.results[existing_columns].copy()
-
- # 按分组和风速排序
- if '分组标识' in curve_data.columns and '风速' in curve_data.columns:
- curve_data = curve_data.sort_values(['分组标识', '风速'])
-
- curve_data.to_csv(output_path, index=False, encoding='utf-8-sig')
- print(f"推力曲线数据已保存到: {output_path}")
- return True
- except Exception as e:
- print(f"保存推力曲线数据时发生错误: {e}")
- return False
-
- def get_summary(self) -> Dict:
- """获取计算结果的统计摘要"""
- if self.results is None:
- return None
-
- # 获取有效数据(风速>0)
- valid_data = self.results[self.results['风速'] > 0]
-
- summary = {
- '总记录数': len(self.results),
- '有效计算记录数': len(valid_data),
- '总分组数': self.total_groups,
- '标准机型数量': self.results['标准机型'].nunique(),
- '描述类型数量': self.results['描述'].nunique() if '描述' in self.results.columns else 0,
- '最大气动推力系数': valid_data['气动推力系数_Ct'].max() if len(valid_data) > 0 else 0,
- '最小气动推力系数': valid_data['气动推力系数_Ct'].min() if len(valid_data) > 0 else 0,
- '平均气动推力系数': valid_data['气动推力系数_Ct'].mean() if len(valid_data) > 0 else 0,
- '最大气动推力_N': f"{valid_data['气动推力_F_N'].max():.1f}" if len(valid_data) > 0 else "0",
- '风速范围_m_s': f"{self.results['风速'].min():.1f} - {self.results['风速'].max():.1f}",
- '叶轮直径范围_m': f"{self.results['叶轮直径_D_m'].min():.1f} - {self.results['叶轮直径_D_m'].max():.1f}" if '叶轮直径_D_m' in self.results.columns else "未知",
- '空气密度范围_kg_m3': f"{self.results['空气密度'].min():.3f} - {self.results['空气密度'].max():.3f}" if '空气密度' in self.results.columns else "未知"
- }
-
- return summary
-
- def print_summary(self) -> None:
- """打印计算摘要"""
- summary = self.get_summary()
- if summary:
- print("\n" + "="*60)
- print("气动推力系数计算摘要")
- print("="*60)
- for key, value in summary.items():
- print(f"{key:25}: {value}")
- print("="*60)
-
- def plot_all_groups_thrust_curves(self, output_dir: str = None) -> bool:
- """
- 为所有分组绘制气动推力系数曲线
-
- Args:
- output_dir: 输出目录
- """
- if self.results is None:
- print("错误:请先计算气动推力系数")
- return False
-
- # 设置中文字体
- plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
- plt.rcParams['axes.unicode_minus'] = False
-
- # 创建输出目录
- if output_dir is None:
- output_dir = "output/plots"
- Path(output_dir).mkdir(parents=True, exist_ok=True)
-
- # 获取所有分组
- groups = self.results['分组标识'].unique()
- total_groups = len(groups)
-
- print(f"\n开始为所有 {total_groups} 个分组绘制推力系数曲线...")
-
- # 记录绘图进度
- plot_count = 0
- failed_plots = []
-
- # 为每个分组绘制图表
- for i, group_id in enumerate(groups, 1):
- try:
- group_data = self.results[self.results['分组标识'] == group_id]
-
- # 创建图形
- fig, axes = plt.subplots(2, 2, figsize=(14, 10))
-
- # 获取机型信息
- turbine_model = group_data['标准机型'].iloc[0] if '标准机型' in group_data.columns else '未知'
- description = group_data['描述'].iloc[0] if '描述' in group_data.columns else ''
- short_desc = description[:30] + "..." if len(description) > 30 else description
-
- # 1. 气动推力系数曲线
- ax1 = axes[0, 0]
- valid_data = group_data[group_data['风速'] > 0]
- if len(valid_data) > 0:
- ax1.plot(valid_data['风速'], valid_data['气动推力系数_Ct'],
- 'b-', linewidth=2, marker='o', markersize=4)
-
- # 标记最大推力系数点
- max_ct_idx = valid_data['气动推力系数_Ct'].idxmax()
- max_ct_wind = valid_data.loc[max_ct_idx, '风速']
- max_ct_value = valid_data.loc[max_ct_idx, '气动推力系数_Ct']
- ax1.plot(max_ct_wind, max_ct_value, 'ro', markersize=8)
-
- # 添加统计信息
- stats_text = f'平均Ct: {valid_data["气动推力系数_Ct"].mean():.3f}\n'
- stats_text += f'最大Ct: {max_ct_value:.3f}\n'
- stats_text += f'风速范围: {group_data["风速"].min():.1f}-{group_data["风速"].max():.1f} m/s'
- ax1.text(0.02, 0.98, stats_text, transform=ax1.transAxes,
- fontsize=10, verticalalignment='top',
- bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
-
- ax1.set_xlabel('风速 (m/s)', fontsize=12)
- ax1.set_ylabel('气动推力系数 Ct', fontsize=12)
- ax1.set_title('气动推力系数曲线', fontsize=14, fontweight='bold')
- ax1.grid(True, alpha=0.3)
-
- # 2. 功率曲线
- ax2 = axes[0, 1]
- ax2.plot(group_data['风速'], group_data['有功功率'],
- 'g-', linewidth=2, marker='s', markersize=4)
- ax2.set_xlabel('风速 (m/s)', fontsize=12)
- ax2.set_ylabel('功率 (kW)', fontsize=12)
- ax2.set_title('功率曲线', fontsize=14, fontweight='bold')
- ax2.grid(True, alpha=0.3)
-
- # 标记额定功率
- rated_power = group_data['有功功率'].max()
- if rated_power > 0:
- rated_wind = group_data[group_data['有功功率'] == rated_power]['风速'].min()
- ax2.plot(rated_wind, rated_power, 'ro', markersize=8)
- ax2.annotate(f'额定: {rated_power} kW\n风速: {rated_wind:.1f} m/s',
- xy=(rated_wind, rated_power),
- xytext=(rated_wind + 2, rated_power * 0.7),
- arrowprops=dict(arrowstyle='->', color='red'),
- fontsize=10, color='red')
-
- # 3. 气动推力曲线
- ax3 = axes[1, 0]
- if len(valid_data) > 0:
- ax3.plot(valid_data['风速'], valid_data['气动推力_F_N'],
- 'r-', linewidth=2, marker='^', markersize=4)
- ax3.set_xlabel('风速 (m/s)', fontsize=12)
- ax3.set_ylabel('气动推力 (N)', fontsize=12)
- ax3.set_title('气动推力曲线', fontsize=14, fontweight='bold')
- ax3.grid(True, alpha=0.3)
-
- # 标记最大推力点
- max_thrust_idx = valid_data['气动推力_F_N'].idxmax()
- max_thrust_wind = valid_data.loc[max_thrust_idx, '风速']
- max_thrust_value = valid_data.loc[max_thrust_idx, '气动推力_F_N']
- ax3.plot(max_thrust_wind, max_thrust_value, 'bo', markersize=8)
-
- # 4. 推力系数与功率关系
- ax4 = axes[1, 1]
- if len(valid_data) > 0:
- scatter = ax4.scatter(valid_data['风速'], valid_data['气动推力系数_Ct'],
- c=valid_data['有功功率'], cmap='viridis', s=50, alpha=0.7)
- ax4.set_xlabel('风速 (m/s)', fontsize=12)
- ax4.set_ylabel('气动推力系数 Ct', fontsize=12)
- ax4.set_title('推力系数与风速关系(颜色表示功率)', fontsize=14, fontweight='bold')
- ax4.grid(True, alpha=0.3)
-
- # 添加颜色条
- cbar = plt.colorbar(scatter, ax=ax4)
- cbar.set_label('功率 (kW)', fontsize=12)
-
- # 设置主标题
- fig.suptitle(f'{turbine_model} - {short_desc}\n分组: {group_id}',
- fontsize=16, fontweight='bold')
-
- # 调整布局
- plt.tight_layout()
-
- # 保存图像
- safe_group_id = group_id.replace('/', '_').replace('\\', '_').replace(':', '_')
- safe_group_id = re.sub(r'[<>:"|?*]', '_', safe_group_id) # 移除Windows文件名非法字符
- plot_path = Path(output_dir) / f"{safe_group_id}_推力系数曲线.png"
- plt.savefig(plot_path, dpi=300, bbox_inches='tight')
- plt.close(fig)
-
- plot_count += 1
- if i % 10 == 0 or i == total_groups:
- print(f" 进度: {i}/{total_groups} ({i/total_groups*100:.1f}%) - 已保存: {plot_path}")
-
- except Exception as e:
- failed_plots.append((group_id, str(e)))
- print(f" 警告: 分组 {group_id} 绘图失败: {e}")
- continue
-
- # 绘制汇总图
- self.plot_summary_analysis(output_dir)
-
- print(f"\n图表绘制完成!")
- print(f" 成功绘制: {plot_count}/{total_groups} 个分组")
-
- if failed_plots:
- print(f" 失败分组: {len(failed_plots)} 个")
- # 保存失败记录
- failed_path = Path(output_dir) / "绘图失败记录.txt"
- with open(failed_path, 'w', encoding='utf-8') as f:
- for group_id, error in failed_plots:
- f.write(f"{group_id}: {error}\n")
- print(f" 失败记录已保存到: {failed_path}")
-
- return True
-
- def plot_summary_analysis(self, output_dir: str) -> bool:
- """绘制汇总分析图"""
- if self.results is None:
- return False
-
- # 设置中文字体
- plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
- plt.rcParams['axes.unicode_minus'] = False
-
- # 计算每个分组的统计信息
- group_stats = []
- for group_id, group_data in self.results.groupby('分组标识'):
- valid_data = group_data[group_data['风速'] > 0]
- if len(valid_data) > 0:
- # 提取机型名称
- parts = group_id.split('_')
- turbine_model = parts[0] if parts else group_id
-
- group_stats.append({
- 'group_id': group_id,
- 'turbine_model': turbine_model,
- 'avg_ct': valid_data['气动推力系数_Ct'].mean(),
- 'max_ct': valid_data['气动推力系数_Ct'].max(),
- 'avg_thrust': valid_data['气动推力_F_N'].mean(),
- 'max_thrust': valid_data['气动推力_F_N'].max(),
- 'rated_power': group_data['有功功率'].max(),
- 'rotor_diameter': group_data['叶轮直径_D_m'].iloc[0] if '叶轮直径_D_m' in group_data.columns else 0,
- 'air_density': group_data['空气密度'].mean() if '空气密度' in group_data.columns else 0
- })
-
- if not group_stats:
- return False
-
- stats_df = pd.DataFrame(group_stats)
-
- # 创建汇总分析图
- fig, axes = plt.subplots(2, 2, figsize=(15, 12))
-
- # 1. 各机型平均推力系数比较
- ax1 = axes[0, 0]
- if 'turbine_model' in stats_df.columns and 'avg_ct' in stats_df.columns:
- model_avg = stats_df.groupby('turbine_model')['avg_ct'].mean().sort_values(ascending=False)
- bars = ax1.barh(range(len(model_avg)), model_avg.values)
- ax1.set_yticks(range(len(model_avg)))
- ax1.set_yticklabels(model_avg.index)
- ax1.set_xlabel('平均气动推力系数 Ct', fontsize=12)
- ax1.set_title('各机型平均推力系数比较', fontsize=14, fontweight='bold')
- ax1.grid(True, alpha=0.3, axis='x')
-
- # 在柱状图上显示数值
- for i, (bar, value) in enumerate(zip(bars, model_avg.values)):
- ax1.text(value, i, f' {value:.3f}', va='center', fontsize=9)
-
- # 2. 推力系数分布直方图
- ax2 = axes[0, 1]
- if 'avg_ct' in stats_df.columns:
- ax2.hist(stats_df['avg_ct'], bins=20, edgecolor='black', alpha=0.7, color='lightcoral')
- ax2.set_xlabel('平均气动推力系数 Ct', fontsize=12)
- ax2.set_ylabel('分组数量', fontsize=12)
- ax2.set_title('推力系数分布直方图', fontsize=14, fontweight='bold')
- ax2.grid(True, alpha=0.3)
-
- # 添加统计信息
- stats_text = f'总分组数: {len(stats_df)}\n'
- stats_text += f'平均值: {stats_df["avg_ct"].mean():.3f}\n'
- stats_text += f'中位数: {stats_df["avg_ct"].median():.3f}\n'
- stats_text += f'标准差: {stats_df["avg_ct"].std():.3f}\n'
- stats_text += f'范围: {stats_df["avg_ct"].min():.3f} - {stats_df["avg_ct"].max():.3f}'
- ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes,
- fontsize=10, verticalalignment='top', horizontalalignment='right',
- bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
-
- # 3. 推力系数与叶轮直径关系
- ax3 = axes[1, 0]
- if 'rotor_diameter' in stats_df.columns and 'avg_ct' in stats_df.columns:
- # 过滤掉直径为0的数据
- valid_diameters = stats_df[stats_df['rotor_diameter'] > 0]
- if len(valid_diameters) > 0:
- scatter = ax3.scatter(valid_diameters['rotor_diameter'], valid_diameters['avg_ct'],
- s=100, alpha=0.6, c=valid_diameters['rated_power'], cmap='viridis')
- ax3.set_xlabel('叶轮直径 (m)', fontsize=12)
- ax3.set_ylabel('平均推力系数 Ct', fontsize=12)
- ax3.set_title('推力系数与叶轮直径关系(颜色表示额定功率)', fontsize=14, fontweight='bold')
- ax3.grid(True, alpha=0.3)
-
- # 添加颜色条
- cbar = plt.colorbar(scatter, ax=ax3)
- cbar.set_label('额定功率 (kW)', fontsize=12)
-
- # 4. 推力系数与额定功率关系
- ax4 = axes[1, 1]
- if 'rated_power' in stats_df.columns and 'avg_ct' in stats_df.columns:
- scatter = ax4.scatter(stats_df['rated_power'], stats_df['avg_ct'],
- s=100, alpha=0.6, c=stats_df['avg_thrust'], cmap='coolwarm')
- ax4.set_xlabel('额定功率 (kW)', fontsize=12)
- ax4.set_ylabel('平均推力系数 Ct', fontsize=12)
- ax4.set_title('推力系数与额定功率关系(颜色表示平均推力)', fontsize=14, fontweight='bold')
- ax4.grid(True, alpha=0.3)
-
- # 添加颜色条
- cbar = plt.colorbar(scatter, ax=ax4)
- cbar.set_label('平均推力 (N)', fontsize=12)
-
- # 设置主标题
- fig.suptitle(f'气动推力系数汇总分析 (共 {len(stats_df)} 个分组)',
- fontsize=16, fontweight='bold')
-
- # 调整布局
- plt.tight_layout()
-
- # 保存图像
- summary_path = Path(output_dir) / "汇总分析图.png"
- plt.savefig(summary_path, dpi=300, bbox_inches='tight')
- plt.close(fig)
-
- print(f" 汇总分析图已保存到: {summary_path}")
- return True
-
- def run(self, output_dir: str = "output", plot_results: bool = True) -> bool:
- """
- 执行完整计算流程
-
- Args:
- output_dir: 输出目录
- plot_results: 是否绘制图表(为所有分组绘制图表)
- """
- print("="*60)
- print("气动推力系数计算器 - 按标准机型和描述分组")
- print("="*60)
-
- # 创建输出目录
- output_path = Path(output_dir)
- output_path.mkdir(parents=True, exist_ok=True)
-
- if not self.load_data():
- return False
-
- if not self.validate_data():
- return False
-
- if not self.calculate_thrust_coefficient():
- return False
-
- self.print_summary()
-
- # 保存计算结果
- print("\n保存计算结果...")
- results_path = output_path / "气动推力系数计算结果.csv"
- self.save_results(results_path)
-
- # 保存分组摘要
- summary_path = output_path / "分组统计摘要.csv"
- self.save_grouped_summary(summary_path)
-
- # 保存推力曲线数据
- curve_path = output_path / "推力曲线数据.csv"
- self.save_thrust_curve_data(curve_path)
-
- # 绘制图表(为所有分组绘制)
- if plot_results:
- print("\n开始为所有分组绘制气动推力系数曲线...")
- plots_dir = output_path / "推力系数曲线图"
-
- # 确认是否继续绘制(如果分组很多)
- if self.total_groups > 50:
- print(f"警告: 共有 {self.total_groups} 个分组,绘制所有图表可能需要较长时间!")
- response = input("是否继续绘制所有图表?(y/n): ")
- if response.lower() != 'y':
- print("跳过图表绘制...")
- else:
- self.plot_all_groups_thrust_curves(plots_dir)
- else:
- self.plot_all_groups_thrust_curves(plots_dir)
-
- # 显示前几行计算结果
- if self.results is not None:
- print("\n前5行计算结果示例:")
- display_cols = ['标准机型', '描述', '风速', '有功功率',
- '气动推力系数_Ct', '气动推力_F_N', '叶轮直径_D_m']
- existing_cols = [col for col in display_cols if col in self.results.columns]
- print(self.results[existing_cols].head())
-
- print("\n" + "="*60)
- print("计算流程完成!")
- print(f"结果文件保存在: {output_path}")
- print("="*60)
- return True
- def main():
- """主函数"""
- # CSV文件路径
- csv_file_path = f"./data/全部机型功率曲线_含标准类型_解析结果.csv" # 替换为您的CSV文件路径
-
- # 创建计算器实例
- calculator = AerodynamicThrustCalculator(csv_file_path)
-
- # 执行计算并绘制图表(为所有分组绘制图表)
- success = calculator.run(
- output_dir="output_results", # 输出目录
- plot_results=True # 为所有分组绘制图表
- )
-
- if success:
- print("\n所有计算和分析已完成!")
-
- # 显示计算结果摘要
- summary = calculator.get_summary()
- if summary:
- print("\n最终结果摘要:")
- for key, value in summary.items():
- print(f"{key:25}: {value}")
- else:
- print("\n计算失败!请检查错误信息。")
- if __name__ == "__main__":
- # 运行主程序
- main()
|