chart_generator.py 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. ##################################################################
  4. #
  5. # Copyright (c) 2023 CICV, Inc. All Rights Reserved
  6. #
  7. ##################################################################
  8. """
  9. @Authors: zhanghaiwen(zhanghaiwen@china-icv.cn)
  10. @Data: 2023/06/25
  11. @Last Modified: 2025/05/20
  12. @Summary: Chart generation utilities for metrics visualization
  13. """
  14. import os
  15. import numpy as np
  16. import pandas as pd
  17. import matplotlib
  18. matplotlib.use('Agg') # 使用非图形界面的后端
  19. import matplotlib.pyplot as plt
  20. from typing import Optional, Dict, List, Any, Union
  21. from pathlib import Path
  22. from modules.lib.log_manager import LogManager
  23. def generate_function_chart_data(function_calculator, metric_name: str, output_dir: Optional[str] = None) -> Optional[str]:
  24. """
  25. Generate chart data for function metrics
  26. Args:
  27. function_calculator: FunctionCalculator instance
  28. metric_name: Metric name
  29. output_dir: Output directory
  30. Returns:
  31. str: Chart file path, or None if generation fails
  32. """
  33. logger = LogManager().get_logger()
  34. try:
  35. # 确保输出目录存在
  36. if output_dir:
  37. os.makedirs(output_dir, exist_ok=True)
  38. else:
  39. output_dir = os.getcwd()
  40. # 根据指标名称选择不同的图表生成方法
  41. if metric_name.lower() == 'latestwarningdistance_ttc_lst':
  42. return generate_warning_ttc_chart(function_calculator, output_dir)
  43. else:
  44. logger.warning(f"Chart generation not implemented for metric [{metric_name}]")
  45. return None
  46. except Exception as e:
  47. logger.error(f"Failed to generate chart data: {str(e)}", exc_info=True)
  48. return None
  49. def generate_warning_ttc_chart(function_calculator, output_dir: str) -> Optional[str]:
  50. """
  51. Generate TTC warning chart with data visualization.
  52. This version first saves data to CSV, then uses the CSV to generate the chart.
  53. Args:
  54. function_calculator: FunctionCalculator instance
  55. output_dir: Output directory
  56. Returns:
  57. str: Chart file path, or None if generation fails
  58. """
  59. logger = LogManager().get_logger()
  60. try:
  61. # 获取数据
  62. ego_df = function_calculator.ego_data.copy()
  63. scenario_name = function_calculator.data.function_config["function"]["scenario"]["name"]
  64. correctwarning = scenario_sign_dict[scenario_name]
  65. warning_dist = calculate_distance(ego_df, correctwarning)
  66. warning_speed = calculate_relative_speed(ego_df, correctwarning)
  67. if warning_dist.empty:
  68. logger.warning("Cannot generate TTC warning chart: empty data")
  69. return None
  70. # 生成时间戳
  71. import datetime
  72. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  73. # 保存 CSV 数据
  74. csv_filename = os.path.join(output_dir, f"warning_ttc_data_{timestamp}.csv")
  75. df_csv = pd.DataFrame({
  76. 'simTime': ego_df['simTime'],
  77. 'warning_distance': warning_dist,
  78. 'warning_speed': warning_speed,
  79. 'ttc': warning_dist / warning_speed
  80. })
  81. df_csv.to_csv(csv_filename, index=False)
  82. logger.info(f"Warning TTC data saved to: {csv_filename}")
  83. # 从 CSV 读取数据
  84. df = pd.read_csv(csv_filename)
  85. # 创建图表
  86. plt.figure(figsize=(12, 8), constrained_layout=True)
  87. # 图 1:预警距离
  88. ax1 = plt.subplot(3, 1, 1)
  89. ax1.plot(df['simTime'], df['warning_distance'], 'b-', label='Warning Distance')
  90. ax1.set_xlabel('Time (s)')
  91. ax1.set_ylabel('Distance (m)')
  92. ax1.set_title('Warning Distance Over Time')
  93. ax1.grid(True)
  94. ax1.legend()
  95. # 图 2:相对速度
  96. ax2 = plt.subplot(3, 1, 2)
  97. ax2.plot(df['simTime'], df['warning_speed'], 'g-', label='Relative Speed')
  98. ax2.set_xlabel('Time (s)')
  99. ax2.set_ylabel('Speed (m/s)')
  100. ax2.set_title('Relative Speed Over Time')
  101. ax2.grid(True)
  102. ax2.legend()
  103. # 图 3:TTC
  104. ax3 = plt.subplot(3, 1, 3)
  105. ax3.plot(df['simTime'], df['ttc'], 'r-', label='TTC')
  106. ax3.set_xlabel('Time (s)')
  107. ax3.set_ylabel('TTC (s)')
  108. ax3.set_title('Time To Collision (TTC) Over Time')
  109. ax3.grid(True)
  110. ax3.legend()
  111. # 保存图像
  112. chart_filename = os.path.join(output_dir, f"warning_ttc_chart_{timestamp}.png")
  113. plt.savefig(chart_filename, dpi=300)
  114. plt.close()
  115. logger.info(f"Warning TTC chart saved to: {chart_filename}")
  116. return chart_filename
  117. except Exception as e:
  118. logger.error(f"Failed to generate warning TTC chart: {str(e)}", exc_info=True)
  119. return None
  120. def generate_comfort_chart_data(comfort_calculator, metric_name: str, output_dir: Optional[str] = None) -> Optional[str]:
  121. """
  122. Generate chart data for comfort metrics
  123. Args:
  124. comfort_calculator: ComfortCalculator instance
  125. metric_name: Metric name
  126. output_dir: Output directory
  127. Returns:
  128. str: Chart file path, or None if generation fails
  129. """
  130. logger = LogManager().get_logger()
  131. try:
  132. # 确保输出目录存在
  133. if output_dir:
  134. os.makedirs(output_dir, exist_ok=True)
  135. else:
  136. output_dir = os.getcwd()
  137. # 根据指标名称选择不同的图表生成方法
  138. if metric_name.lower() == 'shake':
  139. return generate_shake_chart(comfort_calculator, output_dir)
  140. elif metric_name.lower() == 'zigzag':
  141. return generate_zigzag_chart(comfort_calculator, output_dir)
  142. elif metric_name.lower() == 'cadence':
  143. return generate_cadence_chart(comfort_calculator, output_dir)
  144. elif metric_name.lower() == 'slambrake':
  145. return generate_slam_brake_chart(comfort_calculator, output_dir)
  146. elif metric_name.lower() == 'slamaccelerate':
  147. return generate_slam_accelerate_chart(comfort_calculator, output_dir)
  148. elif metric_name.lower() == 'vdv':
  149. return generate_vdv_chart(comfort_calculator, output_dir)
  150. elif metric_name.lower() == 'ava_vav':
  151. return generate_ava_vav_chart(comfort_calculator, output_dir)
  152. elif metric_name.lower() == 'msdv':
  153. return generate_msdv_chart(comfort_calculator, output_dir)
  154. elif metric_name.lower() == 'motionsickness':
  155. return generate_motion_sickness_chart(comfort_calculator, output_dir)
  156. else:
  157. logger.warning(f"Chart generation not implemented for metric [{metric_name}]")
  158. return None
  159. except Exception as e:
  160. logger.error(f"Failed to generate chart data: {str(e)}", exc_info=True)
  161. return None
  162. def generate_shake_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  163. """
  164. Generate shake metric chart with orange background for shake events.
  165. This version first saves data to CSV, then uses the CSV to generate the chart.
  166. Args:
  167. comfort_calculator: ComfortCalculator instance
  168. output_dir: Output directory
  169. Returns:
  170. str: Chart file path, or None if generation fails
  171. """
  172. logger = LogManager().get_logger()
  173. try:
  174. # 获取数据
  175. df = comfort_calculator.ego_df.copy()
  176. shake_events = comfort_calculator.shake_events
  177. if df.empty:
  178. logger.warning("Cannot generate shake chart: empty data")
  179. return None
  180. # 生成时间戳
  181. import datetime
  182. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  183. # 保存 CSV 数据(第一步)
  184. csv_filename = os.path.join(output_dir, f"shake_data_{timestamp}.csv")
  185. df_csv = pd.DataFrame({
  186. 'simTime': df['simTime'],
  187. 'lat_acc': df['lat_acc'],
  188. 'lat_acc_rate': df['lat_acc_rate'],
  189. 'speedH_std': df['speedH_std'],
  190. 'lat_acc_threshold': df.get('lat_acc_threshold', pd.Series([None]*len(df))),
  191. 'lat_acc_rate_threshold': 0.5,
  192. 'speedH_std_threshold': df.get('speedH_threshold', pd.Series([None]*len(df))),
  193. })
  194. df_csv.to_csv(csv_filename, index=False)
  195. logger.info(f"Shake data saved to: {csv_filename}")
  196. # 第二步:从 CSV 读取(可验证保存数据无误)
  197. df = pd.read_csv(csv_filename)
  198. # 创建图表(第三步)
  199. import matplotlib.pyplot as plt
  200. plt.figure(figsize=(12, 8), constrained_layout=True)
  201. # 图 1:横向加速度
  202. ax1 = plt.subplot(3, 1, 1)
  203. ax1.plot(df['simTime'], df['lat_acc'], 'b-', label='Lateral Acceleration')
  204. if 'lat_acc_threshold' in df.columns:
  205. ax1.plot(df['simTime'], df['lat_acc_threshold'], 'r--', label='lat_acc_threshold')
  206. for idx, event in enumerate(shake_events):
  207. label = 'Shake Event' if idx == 0 else None
  208. ax1.axvspan(event['start_time'], event['end_time'], alpha=0.3, color='orange', label=label)
  209. ax1.set_xlabel('Time (s)')
  210. ax1.set_ylabel('Lateral Acceleration (m/s²)')
  211. ax1.set_title('Shake Event Detection - Lateral Acceleration')
  212. ax1.grid(True)
  213. ax1.legend()
  214. # 图 2:lat_acc_rate
  215. ax2 = plt.subplot(3, 1, 2)
  216. ax2.plot(df['simTime'], df['lat_acc_rate'], 'g-', label='lat_acc_rate')
  217. ax2.axhline(
  218. y=0.5, color='orange', linestyle='--', linewidth=1.2, label='lat_acc_rate_threshold'
  219. )
  220. for idx, event in enumerate(shake_events):
  221. label = 'Shake Event' if idx == 0 else None
  222. ax2.axvspan(event['start_time'], event['end_time'], alpha=0.3, color='orange', label=label)
  223. ax2.set_xlabel('Time (s)')
  224. ax2.set_ylabel('Angular Velocity (m/s³)')
  225. ax2.set_title('Shake Event Detection - lat_acc_rate')
  226. ax2.grid(True)
  227. ax2.legend()
  228. # 图 3:speedH_std
  229. ax3 = plt.subplot(3, 1, 3)
  230. ax3.plot(df['simTime'], df['speedH_std'], 'b-', label='speedH_std')
  231. if 'speedH_std_threshold' in df.columns:
  232. ax3.plot(df['simTime'], df['speedH_std_threshold'], 'r--', label='speedH_threshold')
  233. for idx, event in enumerate(shake_events):
  234. label = 'Shake Event' if idx == 0 else None
  235. ax3.axvspan(event['start_time'], event['end_time'], alpha=0.3, color='orange', label=label)
  236. ax3.set_xlabel('Time (s)')
  237. ax3.set_ylabel('Angular Velocity (deg/s)')
  238. ax3.set_title('Shake Event Detection - speedH_std')
  239. ax3.grid(True)
  240. ax3.legend()
  241. # 保存图像
  242. chart_filename = os.path.join(output_dir, f"shake_chart_{timestamp}.png")
  243. plt.savefig(chart_filename, dpi=300)
  244. plt.close()
  245. logger.info(f"Shake chart saved to: {chart_filename}")
  246. return chart_filename
  247. except Exception as e:
  248. logger.error(f"Failed to generate shake chart: {str(e)}", exc_info=True)
  249. return None
  250. def generate_zigzag_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  251. """
  252. Generate zigzag metric chart with orange background for zigzag events.
  253. This version first saves data to CSV, then uses the CSV to generate the chart.
  254. Args:
  255. comfort_calculator: ComfortCalculator instance
  256. output_dir: Output directory
  257. Returns:
  258. str: Chart file path, or None if generation fails
  259. """
  260. logger = LogManager().get_logger()
  261. try:
  262. # 获取数据
  263. df = comfort_calculator.ego_df.copy()
  264. zigzag_events = comfort_calculator.discomfort_df[
  265. comfort_calculator.discomfort_df['type'] == 'zigzag'
  266. ].copy()
  267. if df.empty:
  268. logger.warning("Cannot generate zigzag chart: empty data")
  269. return None
  270. # 生成时间戳
  271. import datetime
  272. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  273. # 保存 CSV 数据(第一步)
  274. csv_filename = os.path.join(output_dir, f"zigzag_data_{timestamp}.csv")
  275. df_csv = pd.DataFrame({
  276. 'simTime': df['simTime'],
  277. 'speedH': df['speedH'],
  278. 'posH': df['posH'],
  279. 'min_speedH_threshold': -2.3, # 可替换为动态阈值
  280. 'max_speedH_threshold': 2.3
  281. })
  282. df_csv.to_csv(csv_filename, index=False)
  283. logger.info(f"Zigzag data saved to: {csv_filename}")
  284. # 第二步:从 CSV 读取(可验证保存数据无误)
  285. df = pd.read_csv(csv_filename)
  286. # 创建图表(第三步)
  287. import matplotlib.pyplot as plt
  288. plt.figure(figsize=(12, 8), constrained_layout=True)
  289. # ===== 子图1:Yaw Rate =====
  290. ax1 = plt.subplot(2, 1, 1)
  291. ax1.plot(df['simTime'], df['speedH'], 'g-', label='Yaw Rate')
  292. # 添加 speedH 上下限阈值线
  293. ax1.axhline(y=2.3, color='m', linestyle='--', linewidth=1.2, label='Max Threshold (+2.3)')
  294. ax1.axhline(y=-2.3, color='r', linestyle='--', linewidth=1.2, label='Min Threshold (-2.3)')
  295. # 添加橙色背景:Zigzag Events
  296. for idx, event in zigzag_events.iterrows():
  297. label = 'Zigzag Event' if idx == 0 else None
  298. ax1.axvspan(event['start_time'], event['end_time'],
  299. alpha=0.3, color='orange', label=label)
  300. ax1.set_xlabel('Time (s)')
  301. ax1.set_ylabel('Angular Velocity (deg/s)')
  302. ax1.set_title('Zigzag Event Detection - Yaw Rate')
  303. ax1.grid(True)
  304. ax1.legend(loc='upper left')
  305. # ===== 子图2:Yaw Angle =====
  306. ax2 = plt.subplot(2, 1, 2)
  307. ax2.plot(df['simTime'], df['posH'], 'b-', label='Yaw')
  308. # 添加橙色背景:Zigzag Events
  309. for idx, event in zigzag_events.iterrows():
  310. label = 'Zigzag Event' if idx == 0 else None
  311. ax2.axvspan(event['start_time'], event['end_time'],
  312. alpha=0.3, color='orange', label=label)
  313. ax2.set_xlabel('Time (s)')
  314. ax2.set_ylabel('Yaw (deg)')
  315. ax2.set_title('Zigzag Event Detection - Yaw Angle')
  316. ax2.grid(True)
  317. ax2.legend(loc='upper left')
  318. # 保存图像
  319. chart_filename = os.path.join(output_dir, f"zigzag_chart_{timestamp}.png")
  320. plt.savefig(chart_filename, dpi=300)
  321. plt.close()
  322. logger.info(f"Zigzag chart saved to: {chart_filename}")
  323. return chart_filename
  324. except Exception as e:
  325. logger.error(f"Failed to generate zigzag chart: {str(e)}", exc_info=True)
  326. return None
  327. def generate_cadence_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  328. """
  329. Generate cadence metric chart with orange background for cadence events.
  330. This version first saves data to CSV, then uses the CSV to generate the chart.
  331. Args:
  332. comfort_calculator: ComfortCalculator instance
  333. output_dir: Output directory
  334. Returns:
  335. str: Chart file path, or None if generation fails
  336. """
  337. logger = LogManager().get_logger()
  338. try:
  339. # 获取数据
  340. df = comfort_calculator.ego_df.copy()
  341. cadence_events = comfort_calculator.discomfort_df[comfort_calculator.discomfort_df['type'] == 'cadence'].copy()
  342. if df.empty:
  343. logger.warning("Cannot generate cadence chart: empty data")
  344. return None
  345. # 生成时间戳
  346. import datetime
  347. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  348. # 保存 CSV 数据(第一步)
  349. csv_filename = os.path.join(output_dir, f"cadence_data_{timestamp}.csv")
  350. df_csv = pd.DataFrame({
  351. 'simTime': df['simTime'],
  352. 'lon_acc': df['lon_acc'],
  353. 'v': df['v'],
  354. 'ip_acc': df.get('ip_acc', pd.Series([None]*len(df))),
  355. 'ip_dec': df.get('ip_dec', pd.Series([None]*len(df)))
  356. })
  357. df_csv.to_csv(csv_filename, index=False)
  358. logger.info(f"Cadence data saved to: {csv_filename}")
  359. # 第二步:从 CSV 读取(可验证保存数据无误)
  360. df = pd.read_csv(csv_filename)
  361. # 创建图表(第三步)
  362. import matplotlib.pyplot as plt
  363. plt.figure(figsize=(12, 8), constrained_layout=True)
  364. # 图 1:纵向加速度
  365. ax1 = plt.subplot(2, 1, 1)
  366. ax1.plot(df['simTime'], df['lon_acc'], 'b-', label='Longitudinal Acceleration')
  367. if 'ip_acc' in df.columns and 'ip_dec' in df.columns:
  368. ax1.plot(df['simTime'], df['ip_acc'], 'r--', label='Acceleration Threshold')
  369. ax1.plot(df['simTime'], df['ip_dec'], 'g--', label='Deceleration Threshold')
  370. # 添加橙色背景标识顿挫事件
  371. for idx, event in cadence_events.iterrows():
  372. label = 'Cadence Event' if idx == 0 else None
  373. ax1.axvspan(event['start_time'], event['end_time'],
  374. alpha=0.3, color='orange', label=label)
  375. ax1.set_xlabel('Time (s)')
  376. ax1.set_ylabel('Longitudinal Acceleration (m/s²)')
  377. ax1.set_title('Cadence Event Detection - Longitudinal Acceleration')
  378. ax1.grid(True)
  379. ax1.legend()
  380. # 图 2:速度
  381. ax2 = plt.subplot(2, 1, 2)
  382. ax2.plot(df['simTime'], df['v'], 'g-', label='Velocity')
  383. # 添加橙色背景标识顿挫事件
  384. for idx, event in cadence_events.iterrows():
  385. label = 'Cadence Event' if idx == 0 else None
  386. ax2.axvspan(event['start_time'], event['end_time'],
  387. alpha=0.3, color='orange', label=label)
  388. ax2.set_xlabel('Time (s)')
  389. ax2.set_ylabel('Velocity (m/s)')
  390. ax2.set_title('Cadence Event Detection - Vehicle Speed')
  391. ax2.grid(True)
  392. ax2.legend()
  393. # 保存图像
  394. chart_filename = os.path.join(output_dir, f"cadence_chart_{timestamp}.png")
  395. plt.savefig(chart_filename, dpi=300)
  396. plt.close()
  397. logger.info(f"Cadence chart saved to: {chart_filename}")
  398. return chart_filename
  399. except Exception as e:
  400. logger.error(f"Failed to generate cadence chart: {str(e)}", exc_info=True)
  401. return None
  402. def generate_slam_brake_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  403. """
  404. Generate slam brake metric chart with orange background for slam brake events.
  405. This version first saves data to CSV, then uses the CSV to generate the chart.
  406. Args:
  407. comfort_calculator: ComfortCalculator instance
  408. output_dir: Output directory
  409. Returns:
  410. str: Chart file path, or None if generation fails
  411. """
  412. logger = LogManager().get_logger()
  413. try:
  414. # 获取数据
  415. df = comfort_calculator.ego_df.copy()
  416. slam_brake_events = comfort_calculator.discomfort_df[comfort_calculator.discomfort_df['type'] == 'slam_brake'].copy()
  417. if df.empty:
  418. logger.warning("Cannot generate slam brake chart: empty data")
  419. return None
  420. # 生成时间戳
  421. import datetime
  422. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  423. # 保存 CSV 数据(第一步)
  424. csv_filename = os.path.join(output_dir, f"slam_brake_data_{timestamp}.csv")
  425. df_csv = pd.DataFrame({
  426. 'simTime': df['simTime'],
  427. 'lon_acc': df['lon_acc'],
  428. 'v': df['v'],
  429. 'min_threshold': df.get('ip_dec', pd.Series([None]*len(df))),
  430. 'max_threshold': 0.0
  431. })
  432. df_csv.to_csv(csv_filename, index=False)
  433. logger.info(f"Slam brake data saved to: {csv_filename}")
  434. # 第二步:从 CSV 读取(可验证保存数据无误)
  435. df = pd.read_csv(csv_filename)
  436. # 创建图表(第三步)
  437. plt.figure(figsize=(12, 8), constrained_layout=True)
  438. # 图 1:纵向加速度
  439. ax1 = plt.subplot(2, 1, 1)
  440. ax1.plot(df['simTime'], df['lon_acc'], 'b-', label='Longitudinal Acceleration')
  441. if 'min_threshold' in df.columns:
  442. ax1.plot(df['simTime'], df['min_threshold'], 'r--', label='Deceleration Threshold')
  443. # 添加橙色背景标识急刹车事件
  444. for idx, event in slam_brake_events.iterrows():
  445. label = 'Slam Brake Event' if idx == 0 else None
  446. ax1.axvspan(event['start_time'], event['end_time'],
  447. alpha=0.3, color='orange', label=label)
  448. ax1.set_xlabel('Time (s)')
  449. ax1.set_ylabel('Longitudinal Acceleration (m/s²)')
  450. ax1.set_title('Slam Brake Event Detection - Longitudinal Acceleration')
  451. ax1.grid(True)
  452. ax1.legend()
  453. # 图 2:速度
  454. ax2 = plt.subplot(2, 1, 2)
  455. ax2.plot(df['simTime'], df['v'], 'g-', label='Velocity')
  456. # 添加橙色背景标识急刹车事件
  457. for idx, event in slam_brake_events.iterrows():
  458. label = 'Slam Brake Event' if idx == 0 else None
  459. ax2.axvspan(event['start_time'], event['end_time'],
  460. alpha=0.3, color='orange', label=label)
  461. ax2.set_xlabel('Time (s)')
  462. ax2.set_ylabel('Velocity (m/s)')
  463. ax2.set_title('Slam Brake Event Detection - Vehicle Speed')
  464. ax2.grid(True)
  465. ax2.legend()
  466. # 保存图像
  467. chart_filename = os.path.join(output_dir, f"slam_brake_chart_{timestamp}.png")
  468. plt.savefig(chart_filename, dpi=300)
  469. plt.close()
  470. logger.info(f"Slam brake chart saved to: {chart_filename}")
  471. return chart_filename
  472. except Exception as e:
  473. logger.error(f"Failed to generate slam brake chart: {str(e)}", exc_info=True)
  474. return None
  475. def generate_slam_accelerate_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  476. """
  477. Generate slam accelerate metric chart with orange background for slam accelerate events.
  478. This version first saves data to CSV, then uses the CSV to generate the chart.
  479. Args:
  480. comfort_calculator: ComfortCalculator instance
  481. output_dir: Output directory
  482. Returns:
  483. str: Chart file path, or None if generation fails
  484. """
  485. logger = LogManager().get_logger()
  486. try:
  487. # 获取数据
  488. df = comfort_calculator.ego_df.copy()
  489. slam_accel_events = comfort_calculator.discomfort_df[comfort_calculator.discomfort_df['type'] == 'slam_accel'].copy()
  490. if df.empty:
  491. logger.warning("Cannot generate slam accelerate chart: empty data")
  492. return None
  493. # 生成时间戳
  494. import datetime
  495. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  496. # 保存 CSV 数据(第一步)
  497. csv_filename = os.path.join(output_dir, f"slam_accel_data_{timestamp}.csv")
  498. df_csv = pd.DataFrame({
  499. 'simTime': df['simTime'],
  500. 'lon_acc': df['lon_acc'],
  501. 'v': df['v'],
  502. 'min_threshold': 0.0,
  503. 'max_threshold': df.get('ip_acc', pd.Series([None]*len(df)))
  504. })
  505. df_csv.to_csv(csv_filename, index=False)
  506. logger.info(f"Slam accelerate data saved to: {csv_filename}")
  507. # 第二步:从 CSV 读取(可验证保存数据无误)
  508. df = pd.read_csv(csv_filename)
  509. # 创建图表(第三步)
  510. plt.figure(figsize=(12, 8), constrained_layout=True)
  511. # 图 1:纵向加速度
  512. ax1 = plt.subplot(2, 1, 1)
  513. ax1.plot(df['simTime'], df['lon_acc'], 'b-', label='Longitudinal Acceleration')
  514. if 'max_threshold' in df.columns:
  515. ax1.plot(df['simTime'], df['max_threshold'], 'r--', label='Acceleration Threshold')
  516. # 添加橙色背景标识急加速事件
  517. for idx, event in slam_accel_events.iterrows():
  518. label = 'Slam Accelerate Event' if idx == 0 else None
  519. ax1.axvspan(event['start_time'], event['end_time'],
  520. alpha=0.3, color='orange', label=label)
  521. ax1.set_xlabel('Time (s)')
  522. ax1.set_ylabel('Longitudinal Acceleration (m/s²)')
  523. ax1.set_title('Slam Accelerate Event Detection - Longitudinal Acceleration')
  524. ax1.grid(True)
  525. ax1.legend()
  526. # 图 2:速度
  527. ax2 = plt.subplot(2, 1, 2)
  528. ax2.plot(df['simTime'], df['v'], 'g-', label='Velocity')
  529. # 添加橙色背景标识急加速事件
  530. for idx, event in slam_accel_events.iterrows():
  531. label = 'Slam Accelerate Event' if idx == 0 else None
  532. ax2.axvspan(event['start_time'], event['end_time'],
  533. alpha=0.3, color='orange', label=label)
  534. ax2.set_xlabel('Time (s)')
  535. ax2.set_ylabel('Velocity (m/s)')
  536. ax2.set_title('Slam Accelerate Event Detection - Vehicle Speed')
  537. ax2.grid(True)
  538. ax2.legend()
  539. # 保存图像
  540. chart_filename = os.path.join(output_dir, f"slam_accel_chart_{timestamp}.png")
  541. plt.savefig(chart_filename, dpi=300)
  542. plt.close()
  543. logger.info(f"Slam accelerate chart saved to: {chart_filename}")
  544. return chart_filename
  545. except Exception as e:
  546. logger.error(f"Failed to generate slam accelerate chart: {str(e)}", exc_info=True)
  547. return None
  548. def get_metric_thresholds(calculator, metric_name: str) -> dict:
  549. """
  550. 从配置文件中获取指标的阈值
  551. Args:
  552. calculator: Calculator instance (SafetyCalculator or ComfortCalculator)
  553. metric_name: 指标名称
  554. Returns:
  555. dict: 包含min和max阈值的字典
  556. """
  557. logger = LogManager().get_logger()
  558. thresholds = {"min": None, "max": None}
  559. try:
  560. # 根据计算器类型获取配置
  561. if hasattr(calculator, 'data_processed'):
  562. if hasattr(calculator.data_processed, 'safety_config') and 'safety' in calculator.data_processed.safety_config:
  563. config = calculator.data_processed.safety_config['safety']
  564. metric_type = 'safety'
  565. elif hasattr(calculator.data_processed, 'comfort_config') and 'comfort' in calculator.data_processed.comfort_config:
  566. config = calculator.data_processed.comfort_config['comfort']
  567. metric_type = 'comfort'
  568. else:
  569. logger.warning(f"无法找到{metric_name}的配置信息")
  570. return thresholds
  571. else:
  572. logger.warning(f"计算器没有data_processed属性")
  573. return thresholds
  574. # 递归查找指标配置
  575. def find_metric_config(node, target_name):
  576. if isinstance(node, dict):
  577. if 'name' in node and node['name'].lower() == target_name.lower() and 'min' in node and 'max' in node:
  578. return node
  579. for key, value in node.items():
  580. result = find_metric_config(value, target_name)
  581. if result:
  582. return result
  583. return None
  584. # 查找指标配置
  585. metric_config = find_metric_config(config, metric_name)
  586. if metric_config:
  587. thresholds["min"] = metric_config.get("min")
  588. thresholds["max"] = metric_config.get("max")
  589. logger.info(f"找到{metric_name}的阈值: min={thresholds['min']}, max={thresholds['max']}")
  590. else:
  591. logger.warning(f"在{metric_type}配置中未找到{metric_name}的阈值信息")
  592. except Exception as e:
  593. logger.error(f"获取{metric_name}阈值时出错: {str(e)}", exc_info=True)
  594. return thresholds
  595. def generate_safety_chart_data(safety_calculator, metric_name: str, output_dir: Optional[str] = None) -> Optional[str]:
  596. """
  597. Generate chart data for safety metrics
  598. Args:
  599. safety_calculator: SafetyCalculator instance
  600. metric_name: Metric name
  601. output_dir: Output directory
  602. Returns:
  603. str: Chart file path, or None if generation fails
  604. """
  605. logger = LogManager().get_logger()
  606. try:
  607. # 确保输出目录存在
  608. if output_dir:
  609. os.makedirs(output_dir, exist_ok=True)
  610. else:
  611. output_dir = os.getcwd()
  612. # 根据指标名称选择不同的图表生成方法
  613. if metric_name.lower() == 'ttc':
  614. return generate_ttc_chart(safety_calculator, output_dir)
  615. elif metric_name.lower() == 'mttc':
  616. return generate_mttc_chart(safety_calculator, output_dir)
  617. elif metric_name.lower() == 'thw':
  618. return generate_thw_chart(safety_calculator, output_dir)
  619. elif metric_name.lower() == 'lonsd':
  620. return generate_lonsd_chart(safety_calculator, output_dir)
  621. elif metric_name.lower() == 'latsd':
  622. return generate_latsd_chart(safety_calculator, output_dir)
  623. elif metric_name.lower() == 'btn':
  624. return generate_btn_chart(safety_calculator, output_dir)
  625. elif metric_name.lower() == 'collisionrisk':
  626. return generate_collision_risk_chart(safety_calculator, output_dir)
  627. elif metric_name.lower() == 'collisionseverity':
  628. return generate_collision_severity_chart(safety_calculator, output_dir)
  629. else:
  630. logger.warning(f"Chart generation not implemented for metric [{metric_name}]")
  631. return None
  632. except Exception as e:
  633. logger.error(f"Failed to generate chart data: {str(e)}", exc_info=True)
  634. return None
  635. def generate_ttc_chart(safety_calculator, output_dir: str) -> Optional[str]:
  636. """
  637. Generate TTC metric chart with orange background for unsafe events.
  638. This version first saves data to CSV, then uses the CSV to generate the chart.
  639. Args:
  640. safety_calculator: SafetyCalculator instance
  641. output_dir: Output directory
  642. Returns:
  643. str: Chart file path, or None if generation fails
  644. """
  645. logger = LogManager().get_logger()
  646. try:
  647. # 获取数据
  648. ttc_data = safety_calculator.ttc_data
  649. if not ttc_data:
  650. logger.warning("Cannot generate TTC chart: empty data")
  651. return None
  652. # 创建DataFrame
  653. df = pd.DataFrame(ttc_data)
  654. # 获取阈值
  655. thresholds = get_metric_thresholds(safety_calculator, 'TTC')
  656. min_threshold = thresholds.get('min')
  657. max_threshold = thresholds.get('max')
  658. # 生成时间戳
  659. import datetime
  660. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  661. # 保存 CSV 数据(第一步)
  662. csv_filename = os.path.join(output_dir, f"ttc_data_{timestamp}.csv")
  663. df_csv = pd.DataFrame({
  664. 'simTime': df['simTime'],
  665. 'simFrame': df['simFrame'],
  666. 'TTC': df['TTC'],
  667. 'min_threshold': min_threshold,
  668. 'max_threshold': max_threshold
  669. })
  670. df_csv.to_csv(csv_filename, index=False)
  671. logger.info(f"TTC data saved to: {csv_filename}")
  672. # 第二步:从 CSV 读取(可验证保存数据无误)
  673. df = pd.read_csv(csv_filename)
  674. # 检测超阈值事件
  675. unsafe_events = []
  676. if min_threshold is not None:
  677. # 对于TTC,小于最小阈值视为不安全
  678. unsafe_condition = df['TTC'] < min_threshold
  679. event_groups = (unsafe_condition != unsafe_condition.shift()).cumsum()
  680. for _, group in df[unsafe_condition].groupby(event_groups):
  681. if len(group) >= 2: # 至少2帧才算一次事件
  682. start_time = group['simTime'].iloc[0]
  683. end_time = group['simTime'].iloc[-1]
  684. duration = end_time - start_time
  685. if duration >= 0.1: # 只记录持续时间超过0.1秒的事件
  686. unsafe_events.append({
  687. 'start_time': start_time,
  688. 'end_time': end_time,
  689. 'start_frame': group['simFrame'].iloc[0],
  690. 'end_frame': group['simFrame'].iloc[-1],
  691. 'duration': duration,
  692. 'min_ttc': group['TTC'].min()
  693. })
  694. # 创建图表(第三步)
  695. plt.figure(figsize=(12, 8))
  696. plt.plot(df['simTime'], df['TTC'], 'b-', label='TTC')
  697. # 添加阈值线
  698. if min_threshold is not None:
  699. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}s)')
  700. if max_threshold is not None:
  701. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold})')
  702. # 添加橙色背景标识不安全事件
  703. for idx, event in enumerate(unsafe_events):
  704. label = 'Unsafe TTC Event' if idx == 0 else None
  705. plt.axvspan(event['start_time'], event['end_time'],
  706. alpha=0.3, color='orange', label=label)
  707. plt.xlabel('Time (s)')
  708. plt.ylabel('TTC (s)')
  709. plt.title('Time To Collision (TTC) Trend')
  710. plt.grid(True)
  711. plt.legend()
  712. # 保存图像
  713. chart_filename = os.path.join(output_dir, f"ttc_chart_{timestamp}.png")
  714. plt.savefig(chart_filename, dpi=300)
  715. plt.close()
  716. # 记录不安全事件信息
  717. if unsafe_events:
  718. logger.info(f"检测到 {len(unsafe_events)} 个TTC不安全事件")
  719. for i, event in enumerate(unsafe_events):
  720. logger.info(f"TTC不安全事件 #{i+1}: 开始时间={event['start_time']:.2f}s, 结束时间={event['end_time']:.2f}s, 持续时间={event['duration']:.2f}s, 最小TTC={event['min_ttc']:.2f}s")
  721. logger.info(f"TTC chart saved to: {chart_filename}")
  722. return chart_filename
  723. except Exception as e:
  724. logger.error(f"Failed to generate TTC chart: {str(e)}", exc_info=True)
  725. return None
  726. def generate_mttc_chart(safety_calculator, output_dir: str) -> Optional[str]:
  727. """
  728. Generate MTTC metric chart with orange background for unsafe events
  729. Args:
  730. safety_calculator: SafetyCalculator instance
  731. output_dir: Output directory
  732. Returns:
  733. str: Chart file path, or None if generation fails
  734. """
  735. logger = LogManager().get_logger()
  736. try:
  737. # 获取数据
  738. mttc_data = safety_calculator.mttc_data
  739. if not mttc_data:
  740. logger.warning("Cannot generate MTTC chart: empty data")
  741. return None
  742. # 创建DataFrame
  743. df = pd.DataFrame(mttc_data)
  744. # 获取阈值
  745. thresholds = get_metric_thresholds(safety_calculator, 'MTTC')
  746. min_threshold = thresholds.get('min')
  747. max_threshold = thresholds.get('max')
  748. # 检测超阈值事件
  749. unsafe_events = []
  750. if min_threshold is not None:
  751. # 对于MTTC,小于最小阈值视为不安全
  752. unsafe_condition = df['MTTC'] < min_threshold
  753. event_groups = (unsafe_condition != unsafe_condition.shift()).cumsum()
  754. for _, group in df[unsafe_condition].groupby(event_groups):
  755. if len(group) >= 2: # 至少2帧才算一次事件
  756. start_time = group['simTime'].iloc[0]
  757. end_time = group['simTime'].iloc[-1]
  758. duration = end_time - start_time
  759. if duration >= 0.1: # 只记录持续时间超过0.1秒的事件
  760. unsafe_events.append({
  761. 'start_time': start_time,
  762. 'end_time': end_time,
  763. 'start_frame': group['simFrame'].iloc[0],
  764. 'end_frame': group['simFrame'].iloc[-1],
  765. 'duration': duration,
  766. 'min_mttc': group['MTTC'].min()
  767. })
  768. # 创建图表
  769. plt.figure(figsize=(12, 6))
  770. plt.plot(df['simTime'], df['MTTC'], 'g-', label='MTTC')
  771. # 添加阈值线
  772. if min_threshold is not None:
  773. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}s)')
  774. if max_threshold is not None:
  775. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold})')
  776. # 添加橙色背景标识不安全事件
  777. for idx, event in enumerate(unsafe_events):
  778. label = 'Unsafe MTTC Event' if idx == 0 else None
  779. plt.axvspan(event['start_time'], event['end_time'],
  780. alpha=0.3, color='orange', label=label)
  781. plt.xlabel('Time (s)')
  782. plt.ylabel('MTTC (s)')
  783. plt.title('Modified Time To Collision (MTTC) Trend')
  784. plt.grid(True)
  785. plt.legend()
  786. # 保存图表
  787. import datetime
  788. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  789. chart_filename = os.path.join(output_dir, f"mttc_chart_{timestamp}.png")
  790. plt.savefig(chart_filename, dpi=300)
  791. plt.close()
  792. # 保存CSV数据,包含阈值信息
  793. csv_filename = os.path.join(output_dir, f"mttc_data_{timestamp}.csv")
  794. df_csv = df.copy()
  795. df_csv['min_threshold'] = min_threshold
  796. df_csv['max_threshold'] = max_threshold
  797. df_csv.to_csv(csv_filename, index=False)
  798. # 记录不安全事件信息
  799. if unsafe_events:
  800. logger.info(f"检测到 {len(unsafe_events)} 个MTTC不安全事件")
  801. for i, event in enumerate(unsafe_events):
  802. logger.info(f"MTTC不安全事件 #{i+1}: 开始时间={event['start_time']:.2f}s, 结束时间={event['end_time']:.2f}s, 持续时间={event['duration']:.2f}s, 最小MTTC={event['min_mttc']:.2f}s")
  803. logger.info(f"MTTC chart saved to: {chart_filename}")
  804. logger.info(f"MTTC data saved to: {csv_filename}")
  805. return chart_filename
  806. except Exception as e:
  807. logger.error(f"Failed to generate MTTC chart: {str(e)}", exc_info=True)
  808. return None
  809. def generate_thw_chart(safety_calculator, output_dir: str) -> Optional[str]:
  810. """
  811. Generate THW metric chart with orange background for unsafe events
  812. Args:
  813. safety_calculator: SafetyCalculator instance
  814. output_dir: Output directory
  815. Returns:
  816. str: Chart file path, or None if generation fails
  817. """
  818. logger = LogManager().get_logger()
  819. try:
  820. # 获取数据
  821. thw_data = safety_calculator.thw_data
  822. if not thw_data:
  823. logger.warning("Cannot generate THW chart: empty data")
  824. return None
  825. # 创建DataFrame
  826. df = pd.DataFrame(thw_data)
  827. # 获取阈值
  828. thresholds = get_metric_thresholds(safety_calculator, 'THW')
  829. min_threshold = thresholds.get('min')
  830. max_threshold = thresholds.get('max')
  831. # 检测超阈值事件
  832. unsafe_events = []
  833. if min_threshold is not None:
  834. # 对于THW,小于最小阈值视为不安全
  835. unsafe_condition = df['THW'] < min_threshold
  836. event_groups = (unsafe_condition != unsafe_condition.shift()).cumsum()
  837. for _, group in df[unsafe_condition].groupby(event_groups):
  838. if len(group) >= 2: # 至少2帧才算一次事件
  839. start_time = group['simTime'].iloc[0]
  840. end_time = group['simTime'].iloc[-1]
  841. duration = end_time - start_time
  842. if duration >= 0.1: # 只记录持续时间超过0.1秒的事件
  843. unsafe_events.append({
  844. 'start_time': start_time,
  845. 'end_time': end_time,
  846. 'start_frame': group['simFrame'].iloc[0],
  847. 'end_frame': group['simFrame'].iloc[-1],
  848. 'duration': duration,
  849. 'min_thw': group['THW'].min()
  850. })
  851. # 创建图表
  852. plt.figure(figsize=(12, 6))
  853. plt.plot(df['simTime'], df['THW'], 'c-', label='THW')
  854. # 添加阈值线
  855. if min_threshold is not None:
  856. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}s)')
  857. if max_threshold is not None:
  858. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold})')
  859. # 添加橙色背景标识不安全事件
  860. for idx, event in enumerate(unsafe_events):
  861. label = 'Unsafe THW Event' if idx == 0 else None
  862. plt.axvspan(event['start_time'], event['end_time'],
  863. alpha=0.3, color='orange', label=label)
  864. plt.xlabel('Time (s)')
  865. plt.ylabel('THW (s)')
  866. plt.title('Time Headway (THW) Trend')
  867. plt.grid(True)
  868. plt.legend()
  869. # 保存图表
  870. import datetime
  871. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  872. chart_filename = os.path.join(output_dir, f"thw_chart_{timestamp}.png")
  873. plt.savefig(chart_filename, dpi=300)
  874. plt.close()
  875. # 保存CSV数据,包含阈值信息
  876. csv_filename = os.path.join(output_dir, f"thw_data_{timestamp}.csv")
  877. df_csv = df.copy()
  878. df_csv['min_threshold'] = min_threshold
  879. df_csv['max_threshold'] = max_threshold
  880. df_csv.to_csv(csv_filename, index=False)
  881. # 记录不安全事件信息
  882. if unsafe_events:
  883. logger.info(f"检测到 {len(unsafe_events)} 个THW不安全事件")
  884. for i, event in enumerate(unsafe_events):
  885. logger.info(f"THW不安全事件 #{i+1}: 开始时间={event['start_time']:.2f}s, 结束时间={event['end_time']:.2f}s, 持续时间={event['duration']:.2f}s, 最小THW={event['min_thw']:.2f}s")
  886. logger.info(f"THW chart saved to: {chart_filename}")
  887. logger.info(f"THW data saved to: {csv_filename}")
  888. return chart_filename
  889. except Exception as e:
  890. logger.error(f"Failed to generate THW chart: {str(e)}", exc_info=True)
  891. return None
  892. def generate_lonsd_chart(safety_calculator, output_dir: str) -> Optional[str]:
  893. """
  894. Generate Longitudinal Safe Distance metric chart
  895. Args:
  896. safety_calculator: SafetyCalculator instance
  897. output_dir: Output directory
  898. Returns:
  899. str: Chart file path, or None if generation fails
  900. """
  901. logger = LogManager().get_logger()
  902. try:
  903. # 获取数据
  904. lonsd_data = safety_calculator.lonsd_data
  905. if not lonsd_data:
  906. logger.warning("Cannot generate Longitudinal Safe Distance chart: empty data")
  907. return None
  908. # 创建DataFrame
  909. df = pd.DataFrame(lonsd_data)
  910. # 获取阈值
  911. thresholds = get_metric_thresholds(safety_calculator, 'LonSD')
  912. min_threshold = thresholds.get('min')
  913. max_threshold = thresholds.get('max')
  914. # 创建图表
  915. plt.figure(figsize=(12, 6))
  916. plt.plot(df['simTime'], df['LonSD'], 'm-', label='Longitudinal Safe Distance')
  917. # 添加阈值线
  918. if min_threshold is not None:
  919. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}m)')
  920. if max_threshold is not None:
  921. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold}m)')
  922. plt.xlabel('Time (s)')
  923. plt.ylabel('Distance (m)')
  924. plt.title('Longitudinal Safe Distance (LonSD) Trend')
  925. plt.grid(True)
  926. plt.legend()
  927. # 保存图表
  928. import datetime
  929. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  930. chart_filename = os.path.join(output_dir, f"lonsd_chart_{timestamp}.png")
  931. plt.savefig(chart_filename, dpi=300)
  932. plt.close()
  933. # 保存CSV数据,包含阈值信息
  934. csv_filename = os.path.join(output_dir, f"lonsd_data_{timestamp}.csv")
  935. df_csv = df.copy()
  936. df_csv['min_threshold'] = min_threshold
  937. df_csv['max_threshold'] = max_threshold
  938. df_csv.to_csv(csv_filename, index=False)
  939. logger.info(f"Longitudinal Safe Distance chart saved to: {chart_filename}")
  940. logger.info(f"Longitudinal Safe Distance data saved to: {csv_filename}")
  941. return chart_filename
  942. except Exception as e:
  943. logger.error(f"Failed to generate Longitudinal Safe Distance chart: {str(e)}", exc_info=True)
  944. return None
  945. def generate_latsd_chart(safety_calculator, output_dir: str) -> Optional[str]:
  946. """
  947. Generate Lateral Safe Distance metric chart with orange background for unsafe events
  948. Args:
  949. safety_calculator: SafetyCalculator instance
  950. output_dir: Output directory
  951. Returns:
  952. str: Chart file path, or None if generation fails
  953. """
  954. logger = LogManager().get_logger()
  955. try:
  956. # 获取数据
  957. latsd_data = safety_calculator.latsd_data
  958. if not latsd_data:
  959. logger.warning("Cannot generate Lateral Safe Distance chart: empty data")
  960. return None
  961. # 创建DataFrame
  962. df = pd.DataFrame(latsd_data)
  963. # 获取阈值
  964. thresholds = get_metric_thresholds(safety_calculator, 'LatSD')
  965. min_threshold = thresholds.get('min')
  966. max_threshold = thresholds.get('max')
  967. # 检测超阈值事件
  968. unsafe_events = []
  969. if min_threshold is not None:
  970. # 对于LatSD,小于最小阈值视为不安全
  971. unsafe_condition = df['LatSD'] < min_threshold
  972. event_groups = (unsafe_condition != unsafe_condition.shift()).cumsum()
  973. for _, group in df[unsafe_condition].groupby(event_groups):
  974. if len(group) >= 2: # 至少2帧才算一次事件
  975. start_time = group['simTime'].iloc[0]
  976. end_time = group['simTime'].iloc[-1]
  977. duration = end_time - start_time
  978. if duration >= 0.1: # 只记录持续时间超过0.1秒的事件
  979. unsafe_events.append({
  980. 'start_time': start_time,
  981. 'end_time': end_time,
  982. 'start_frame': group['simFrame'].iloc[0],
  983. 'end_frame': group['simFrame'].iloc[-1],
  984. 'duration': duration,
  985. 'min_latsd': group['LatSD'].min()
  986. })
  987. # 创建图表
  988. plt.figure(figsize=(12, 6))
  989. plt.plot(df['simTime'], df['LatSD'], 'y-', label='Lateral Safe Distance')
  990. # 添加阈值线
  991. if min_threshold is not None:
  992. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}m)')
  993. if max_threshold is not None:
  994. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold}m)')
  995. # 添加橙色背景标识不安全事件
  996. for idx, event in enumerate(unsafe_events):
  997. label = 'Unsafe LatSD Event' if idx == 0 else None
  998. plt.axvspan(event['start_time'], event['end_time'],
  999. alpha=0.3, color='orange', label=label)
  1000. plt.xlabel('Time (s)')
  1001. plt.ylabel('Distance (m)')
  1002. plt.title('Lateral Safe Distance (LatSD) Trend')
  1003. plt.grid(True)
  1004. plt.legend()
  1005. # 保存图表
  1006. import datetime
  1007. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1008. chart_filename = os.path.join(output_dir, f"latsd_chart_{timestamp}.png")
  1009. plt.savefig(chart_filename, dpi=300)
  1010. plt.close()
  1011. # 保存CSV数据,包含阈值信息
  1012. csv_filename = os.path.join(output_dir, f"latsd_data_{timestamp}.csv")
  1013. df_csv = df.copy()
  1014. df_csv['min_threshold'] = min_threshold
  1015. df_csv['max_threshold'] = max_threshold
  1016. df_csv.to_csv(csv_filename, index=False)
  1017. # 记录不安全事件信息
  1018. if unsafe_events:
  1019. logger.info(f"检测到 {len(unsafe_events)} 个LatSD不安全事件")
  1020. for i, event in enumerate(unsafe_events):
  1021. logger.info(f"LatSD不安全事件 #{i+1}: 开始时间={event['start_time']:.2f}s, 结束时间={event['end_time']:.2f}s, 持续时间={event['duration']:.2f}s, 最小LatSD={event['min_latsd']:.2f}m")
  1022. logger.info(f"Lateral Safe Distance chart saved to: {chart_filename}")
  1023. logger.info(f"Lateral Safe Distance data saved to: {csv_filename}")
  1024. return chart_filename
  1025. except Exception as e:
  1026. logger.error(f"Failed to generate Lateral Safe Distance chart: {str(e)}", exc_info=True)
  1027. return None
  1028. def generate_btn_chart(safety_calculator, output_dir: str) -> Optional[str]:
  1029. """
  1030. Generate Brake Threat Number metric chart with orange background for unsafe events
  1031. Args:
  1032. safety_calculator: SafetyCalculator instance
  1033. output_dir: Output directory
  1034. Returns:
  1035. str: Chart file path, or None if generation fails
  1036. """
  1037. logger = LogManager().get_logger()
  1038. try:
  1039. # 获取数据
  1040. btn_data = safety_calculator.btn_data
  1041. if not btn_data:
  1042. logger.warning("Cannot generate Brake Threat Number chart: empty data")
  1043. return None
  1044. # 创建DataFrame
  1045. df = pd.DataFrame(btn_data)
  1046. # 获取阈值
  1047. thresholds = get_metric_thresholds(safety_calculator, 'BTN')
  1048. min_threshold = thresholds.get('min')
  1049. max_threshold = thresholds.get('max')
  1050. # 检测超阈值事件
  1051. unsafe_events = []
  1052. if max_threshold is not None:
  1053. # 对于BTN,大于最大阈值视为不安全
  1054. unsafe_condition = df['BTN'] > max_threshold
  1055. event_groups = (unsafe_condition != unsafe_condition.shift()).cumsum()
  1056. for _, group in df[unsafe_condition].groupby(event_groups):
  1057. if len(group) >= 2: # 至少2帧才算一次事件
  1058. start_time = group['simTime'].iloc[0]
  1059. end_time = group['simTime'].iloc[-1]
  1060. duration = end_time - start_time
  1061. if duration >= 0.1: # 只记录持续时间超过0.1秒的事件
  1062. unsafe_events.append({
  1063. 'start_time': start_time,
  1064. 'end_time': end_time,
  1065. 'start_frame': group['simFrame'].iloc[0],
  1066. 'end_frame': group['simFrame'].iloc[-1],
  1067. 'duration': duration,
  1068. 'max_btn': group['BTN'].max()
  1069. })
  1070. # 创建图表
  1071. plt.figure(figsize=(12, 6))
  1072. plt.plot(df['simTime'], df['BTN'], 'r-', label='Brake Threat Number')
  1073. # 添加阈值线
  1074. if min_threshold is not None:
  1075. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold})')
  1076. if max_threshold is not None:
  1077. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold})')
  1078. # 添加橙色背景标识不安全事件
  1079. for idx, event in enumerate(unsafe_events):
  1080. label = 'Unsafe BTN Event' if idx == 0 else None
  1081. plt.axvspan(event['start_time'], event['end_time'],
  1082. alpha=0.3, color='orange', label=label)
  1083. plt.xlabel('Time (s)')
  1084. plt.ylabel('BTN')
  1085. plt.title('Brake Threat Number (BTN) Trend')
  1086. plt.grid(True)
  1087. plt.legend()
  1088. # 保存图表
  1089. import datetime
  1090. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1091. chart_filename = os.path.join(output_dir, f"btn_chart_{timestamp}.png")
  1092. plt.savefig(chart_filename, dpi=300)
  1093. plt.close()
  1094. # 保存CSV数据,包含阈值信息
  1095. csv_filename = os.path.join(output_dir, f"btn_data_{timestamp}.csv")
  1096. df_csv = df.copy()
  1097. df_csv['min_threshold'] = min_threshold
  1098. df_csv['max_threshold'] = max_threshold
  1099. df_csv.to_csv(csv_filename, index=False)
  1100. # 记录不安全事件信息
  1101. if unsafe_events:
  1102. logger.info(f"检测到 {len(unsafe_events)} 个BTN不安全事件")
  1103. for i, event in enumerate(unsafe_events):
  1104. logger.info(f"BTN不安全事件 #{i+1}: 开始时间={event['start_time']:.2f}s, 结束时间={event['end_time']:.2f}s, 持续时间={event['duration']:.2f}s, 最大BTN={event['max_btn']:.2f}")
  1105. logger.info(f"Brake Threat Number chart saved to: {chart_filename}")
  1106. logger.info(f"Brake Threat Number data saved to: {csv_filename}")
  1107. return chart_filename
  1108. except Exception as e:
  1109. logger.error(f"Failed to generate Brake Threat Number chart: {str(e)}", exc_info=True)
  1110. return None
  1111. def generate_collision_risk_chart(safety_calculator, output_dir: str) -> Optional[str]:
  1112. """
  1113. Generate Collision Risk metric chart
  1114. Args:
  1115. safety_calculator: SafetyCalculator instance
  1116. output_dir: Output directory
  1117. Returns:
  1118. str: Chart file path, or None if generation fails
  1119. """
  1120. logger = LogManager().get_logger()
  1121. try:
  1122. # 获取数据
  1123. risk_data = safety_calculator.collision_risk_data
  1124. if not risk_data:
  1125. logger.warning("Cannot generate Collision Risk chart: empty data")
  1126. return None
  1127. # 创建DataFrame
  1128. df = pd.DataFrame(risk_data)
  1129. # 获取阈值
  1130. thresholds = get_metric_thresholds(safety_calculator, 'collisionRisk')
  1131. min_threshold = thresholds.get('min')
  1132. max_threshold = thresholds.get('max')
  1133. # 创建图表
  1134. plt.figure(figsize=(12, 6))
  1135. plt.plot(df['simTime'], df['collisionRisk'], 'r-', label='Collision Risk')
  1136. # 添加阈值线
  1137. if min_threshold is not None:
  1138. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}%)')
  1139. if max_threshold is not None:
  1140. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold}%)')
  1141. plt.xlabel('Time (s)')
  1142. plt.ylabel('Risk Value (%)')
  1143. plt.title('Collision Risk (collisionRisk) Trend')
  1144. plt.grid(True)
  1145. plt.legend()
  1146. # 保存图表
  1147. import datetime
  1148. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1149. chart_filename = os.path.join(output_dir, f"collision_risk_chart_{timestamp}.png")
  1150. plt.savefig(chart_filename, dpi=300)
  1151. plt.close()
  1152. # 保存CSV数据,包含阈值信息
  1153. csv_filename = os.path.join(output_dir, f"collisionrisk_data_{timestamp}.csv")
  1154. df_csv = df.copy()
  1155. df_csv['min_threshold'] = min_threshold
  1156. df_csv['max_threshold'] = max_threshold
  1157. df_csv.to_csv(csv_filename, index=False)
  1158. logger.info(f"Collision Risk chart saved to: {chart_filename}")
  1159. logger.info(f"Collision Risk data saved to: {csv_filename}")
  1160. return chart_filename
  1161. except Exception as e:
  1162. logger.error(f"Failed to generate Collision Risk chart: {str(e)}", exc_info=True)
  1163. return None
  1164. def generate_collision_severity_chart(safety_calculator, output_dir: str) -> Optional[str]:
  1165. """
  1166. Generate Collision Severity metric chart
  1167. Args:
  1168. safety_calculator: SafetyCalculator instance
  1169. output_dir: Output directory
  1170. Returns:
  1171. str: Chart file path, or None if generation fails
  1172. """
  1173. logger = LogManager().get_logger()
  1174. try:
  1175. # 获取数据
  1176. severity_data = safety_calculator.collision_severity_data
  1177. if not severity_data:
  1178. logger.warning("Cannot generate Collision Severity chart: empty data")
  1179. return None
  1180. # 创建DataFrame
  1181. df = pd.DataFrame(severity_data)
  1182. # 获取阈值
  1183. thresholds = get_metric_thresholds(safety_calculator, 'collisionSeverity')
  1184. min_threshold = thresholds.get('min')
  1185. max_threshold = thresholds.get('max')
  1186. # 创建图表
  1187. plt.figure(figsize=(12, 6))
  1188. plt.plot(df['simTime'], df['collisionSeverity'], 'r-', label='Collision Severity')
  1189. # 添加阈值线
  1190. if min_threshold is not None:
  1191. plt.axhline(y=min_threshold, color='r', linestyle='--', label=f'Min Threshold ({min_threshold}%)')
  1192. if max_threshold is not None:
  1193. plt.axhline(y=max_threshold, color='g', linestyle='--', label=f'Max Threshold ({max_threshold}%)')
  1194. plt.xlabel('Time (s)')
  1195. plt.ylabel('Severity (%)')
  1196. plt.title('Collision Severity (collisionSeverity) Trend')
  1197. plt.grid(True)
  1198. plt.legend()
  1199. # 保存图表
  1200. import datetime
  1201. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1202. chart_filename = os.path.join(output_dir, f"collision_severity_chart_{timestamp}.png")
  1203. plt.savefig(chart_filename, dpi=300)
  1204. plt.close()
  1205. # 保存CSV数据,包含阈值信息
  1206. csv_filename = os.path.join(output_dir, f"collisionseverity_data_{timestamp}.csv")
  1207. df_csv = df.copy()
  1208. df_csv['min_threshold'] = min_threshold
  1209. df_csv['max_threshold'] = max_threshold
  1210. df_csv.to_csv(csv_filename, index=False)
  1211. logger.info(f"Collision Severity chart saved to: {chart_filename}")
  1212. logger.info(f"Collision Severity data saved to: {csv_filename}")
  1213. return chart_filename
  1214. except Exception as e:
  1215. logger.error(f"Failed to generate Collision Severity chart: {str(e)}", exc_info=True)
  1216. return None
  1217. def generate_vdv_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  1218. """
  1219. Generate VDV (Vibration Dose Value) metric chart with data saved to CSV first.
  1220. This version first saves data to CSV, then uses the CSV to generate the chart.
  1221. Args:
  1222. comfort_calculator: ComfortCalculator instance
  1223. output_dir: Output directory
  1224. Returns:
  1225. str: Chart file path, or None if generation fails
  1226. """
  1227. logger = LogManager().get_logger()
  1228. try:
  1229. # 获取数据
  1230. df = comfort_calculator.ego_df.copy()
  1231. vdv_value = comfort_calculator.calculated_value.get('vdv', 0)
  1232. if df.empty:
  1233. logger.warning("Cannot generate VDV chart: empty data")
  1234. return None
  1235. # 确保有必要的列
  1236. if 'accelX' not in df.columns or 'accelY' not in df.columns:
  1237. logger.warning("Missing required columns for VDV chart")
  1238. return None
  1239. # 获取阈值
  1240. thresholds = get_metric_thresholds(comfort_calculator, 'vdv')
  1241. min_threshold = thresholds.get('min')
  1242. max_threshold = thresholds.get('max')
  1243. # 将东北天坐标系下的加速度转换为车身坐标系下的加速度
  1244. if 'posH' not in df.columns:
  1245. logger.warning("Missing heading angle data for coordinate transformation")
  1246. return None
  1247. # 车身坐标系:X轴指向车头,Y轴指向车辆左侧,Z轴指向车顶
  1248. df['posH_rad'] = np.radians(df['posH'])
  1249. # 转换加速度到车身坐标系
  1250. df['a_x_body'] = df['accelX'] * np.sin(df['posH_rad']) + df['accelY'] * np.cos(df['posH_rad'])
  1251. df['a_y_body'] = df['accelX'] * np.cos(df['posH_rad']) - df['accelY'] * np.sin(df['posH_rad'])
  1252. df['a_z_body'] = df['accelZ'] if 'accelZ' in df.columns else pd.Series(np.zeros(len(df)))
  1253. # 生成时间戳
  1254. import datetime
  1255. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1256. # 保存 CSV 数据(第一步)
  1257. csv_filename = os.path.join(output_dir, f"vdv_data_{timestamp}.csv")
  1258. df_csv = pd.DataFrame({
  1259. 'simTime': df['simTime'],
  1260. 'a_x_body': df['a_x_body'],
  1261. 'a_y_body': df['a_y_body'],
  1262. 'a_z_body': df['a_z_body'],
  1263. 'v': df['v'],
  1264. 'min_threshold': min_threshold,
  1265. 'max_threshold': max_threshold,
  1266. 'vdv_value': vdv_value
  1267. })
  1268. df_csv.to_csv(csv_filename, index=False)
  1269. logger.info(f"VDV data saved to: {csv_filename}")
  1270. # 第二步:从 CSV 读取(可验证保存数据无误)
  1271. df = pd.read_csv(csv_filename)
  1272. # 创建图表(第三步)
  1273. plt.figure(figsize=(12, 8))
  1274. # 绘制三轴加速度
  1275. plt.subplot(3, 1, 1)
  1276. plt.plot(df['simTime'], df['a_x_body'], 'r-', label='X-axis Acceleration')
  1277. # 添加阈值线
  1278. if 'min_threshold' in df.columns and df['min_threshold'].iloc[0] is not None:
  1279. min_threshold = df['min_threshold'].iloc[0]
  1280. plt.axhline(y=min_threshold, color='r', linestyle=':', label=f'Min Threshold ({min_threshold})')
  1281. if 'max_threshold' in df.columns and df['max_threshold'].iloc[0] is not None:
  1282. max_threshold = df['max_threshold'].iloc[0]
  1283. plt.axhline(y=max_threshold, color='g', linestyle=':', label=f'Max Threshold ({max_threshold})')
  1284. plt.xlabel('Time (s)')
  1285. plt.ylabel('Acceleration (m/s²)')
  1286. plt.title('Body X-axis Acceleration (Longitudinal)')
  1287. plt.grid(True)
  1288. plt.legend()
  1289. plt.subplot(3, 1, 2)
  1290. plt.plot(df['simTime'], df['a_y_body'], 'g-', label='Y-axis Acceleration')
  1291. plt.xlabel('Time (s)')
  1292. plt.ylabel('Acceleration (m/s²)')
  1293. plt.title('Body Y-axis Acceleration (Lateral)')
  1294. plt.grid(True)
  1295. plt.legend()
  1296. plt.subplot(3, 1, 3)
  1297. plt.plot(df['simTime'], df['a_z_body'], 'b-', label='Z-axis Acceleration')
  1298. plt.xlabel('Time (s)')
  1299. plt.ylabel('Acceleration (m/s²)')
  1300. vdv_value = df['vdv_value'].iloc[0] if 'vdv_value' in df.columns else 0
  1301. plt.title(f'Body Z-axis Acceleration (Vertical) - VDV value: {vdv_value:.4f}')
  1302. plt.grid(True)
  1303. plt.legend()
  1304. plt.tight_layout()
  1305. # 保存图像
  1306. chart_filename = os.path.join(output_dir, f"vdv_chart_{timestamp}.png")
  1307. plt.savefig(chart_filename, dpi=300)
  1308. plt.close()
  1309. logger.info(f"VDV chart saved to: {chart_filename}")
  1310. return chart_filename
  1311. except Exception as e:
  1312. logger.error(f"Failed to generate VDV chart: {str(e)}", exc_info=True)
  1313. return None
  1314. def generate_ava_vav_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  1315. """
  1316. Generate AVA_VAV (Average Vibration Acceleration Value) metric chart with data saved to CSV first.
  1317. This version first saves data to CSV, then uses the CSV to generate the chart.
  1318. Args:
  1319. comfort_calculator: ComfortCalculator instance
  1320. output_dir: Output directory
  1321. Returns:
  1322. str: Chart file path, or None if generation fails
  1323. """
  1324. logger = LogManager().get_logger()
  1325. try:
  1326. # 获取数据
  1327. df = comfort_calculator.ego_df.copy()
  1328. ava_vav_value = comfort_calculator.calculated_value.get('ava_vav', 0)
  1329. if df.empty:
  1330. logger.warning("Cannot generate AVA_VAV chart: empty data")
  1331. return None
  1332. # 确保有必要的列
  1333. if 'accelX' not in df.columns or 'accelY' not in df.columns:
  1334. logger.warning("Missing required columns for AVA_VAV chart")
  1335. return None
  1336. # 获取阈值
  1337. thresholds = get_metric_thresholds(comfort_calculator, 'ava_vav')
  1338. min_threshold = thresholds.get('min')
  1339. max_threshold = thresholds.get('max')
  1340. # 将东北天坐标系下的加速度转换为车身坐标系下的加速度
  1341. if 'posH' not in df.columns:
  1342. logger.warning("Missing heading angle data for coordinate transformation")
  1343. return None
  1344. # 车身坐标系:X轴指向车头,Y轴指向车辆左侧,Z轴指向车顶
  1345. df['posH_rad'] = np.radians(df['posH'])
  1346. # 转换加速度到车身坐标系
  1347. df['a_x_body'] = df['accelX'] * np.sin(df['posH_rad']) + df['accelY'] * np.cos(df['posH_rad'])
  1348. df['a_y_body'] = df['accelX'] * np.cos(df['posH_rad']) - df['accelY'] * np.sin(df['posH_rad'])
  1349. df['a_z_body'] = df['accelZ'] if 'accelZ' in df.columns else pd.Series(np.zeros(len(df)))
  1350. # 角速度数据
  1351. df['omega_roll'] = df['rollRate'] if 'rollRate' in df.columns else pd.Series(np.zeros(len(df)))
  1352. df['omega_pitch'] = df['pitchRate'] if 'pitchRate' in df.columns else pd.Series(np.zeros(len(df)))
  1353. df['omega_yaw'] = df['speedH'] # 使用航向角速度作为偏航角速度
  1354. # 生成时间戳
  1355. import datetime
  1356. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1357. # 保存 CSV 数据(第一步)
  1358. csv_filename = os.path.join(output_dir, f"ava_vav_data_{timestamp}.csv")
  1359. df_csv = pd.DataFrame({
  1360. 'simTime': df['simTime'],
  1361. 'a_x_body': df['a_x_body'],
  1362. 'a_y_body': df['a_y_body'],
  1363. 'a_z_body': df['a_z_body'],
  1364. 'omega_roll': df['omega_roll'],
  1365. 'omega_pitch': df['omega_pitch'],
  1366. 'omega_yaw': df['omega_yaw'],
  1367. 'min_threshold': min_threshold,
  1368. 'max_threshold': max_threshold,
  1369. 'ava_vav_value': ava_vav_value
  1370. })
  1371. df_csv.to_csv(csv_filename, index=False)
  1372. logger.info(f"AVA_VAV data saved to: {csv_filename}")
  1373. # 第二步:从 CSV 读取(可验证保存数据无误)
  1374. df = pd.read_csv(csv_filename)
  1375. # 创建图表(第三步)
  1376. plt.figure(figsize=(12, 10))
  1377. # 绘制三轴加速度
  1378. plt.subplot(3, 2, 1)
  1379. plt.plot(df['simTime'], df['a_x_body'], 'r-', label='X-axis Acceleration')
  1380. # 添加阈值线
  1381. if 'min_threshold' in df.columns and df['min_threshold'].iloc[0] is not None:
  1382. min_threshold = df['min_threshold'].iloc[0]
  1383. plt.axhline(y=min_threshold, color='r', linestyle=':', label=f'Min Threshold ({min_threshold})')
  1384. if 'max_threshold' in df.columns and df['max_threshold'].iloc[0] is not None:
  1385. max_threshold = df['max_threshold'].iloc[0]
  1386. plt.axhline(y=max_threshold, color='g', linestyle=':', label=f'Max Threshold ({max_threshold})')
  1387. plt.xlabel('Time (s)')
  1388. plt.ylabel('Acceleration (m/s²)')
  1389. plt.title('Body X-axis Acceleration (Longitudinal)')
  1390. plt.grid(True)
  1391. plt.legend()
  1392. plt.subplot(3, 2, 3)
  1393. plt.plot(df['simTime'], df['a_y_body'], 'g-', label='Y-axis Acceleration')
  1394. plt.xlabel('Time (s)')
  1395. plt.ylabel('Acceleration (m/s²)')
  1396. plt.title('Body Y-axis Acceleration (Lateral)')
  1397. plt.grid(True)
  1398. plt.legend()
  1399. plt.subplot(3, 2, 5)
  1400. plt.plot(df['simTime'], df['a_z_body'], 'b-', label='Z-axis Acceleration')
  1401. plt.xlabel('Time (s)')
  1402. plt.ylabel('Acceleration (m/s²)')
  1403. plt.title('Body Z-axis Acceleration (Vertical)')
  1404. plt.grid(True)
  1405. plt.legend()
  1406. # 绘制三轴角速度
  1407. plt.subplot(3, 2, 2)
  1408. plt.plot(df['simTime'], df['omega_roll'], 'r-', label='Roll Rate')
  1409. plt.xlabel('Time (s)')
  1410. plt.ylabel('Angular Velocity (deg/s)')
  1411. plt.title('Roll Rate')
  1412. plt.grid(True)
  1413. plt.legend()
  1414. plt.subplot(3, 2, 4)
  1415. plt.plot(df['simTime'], df['omega_pitch'], 'g-', label='Pitch Rate')
  1416. plt.xlabel('Time (s)')
  1417. plt.ylabel('Angular Velocity (deg/s)')
  1418. plt.title('Pitch Rate')
  1419. plt.grid(True)
  1420. plt.legend()
  1421. plt.subplot(3, 2, 6)
  1422. plt.plot(df['simTime'], df['omega_yaw'], 'b-', label='Yaw Rate')
  1423. plt.xlabel('Time (s)')
  1424. plt.ylabel('Angular Velocity (deg/s)')
  1425. ava_vav_value = df['ava_vav_value'].iloc[0] if 'ava_vav_value' in df.columns else 0
  1426. plt.title(f'Yaw Rate - AVA_VAV value: {ava_vav_value:.4f}')
  1427. plt.grid(True)
  1428. plt.legend()
  1429. plt.tight_layout()
  1430. # 保存图像
  1431. chart_filename = os.path.join(output_dir, f"ava_vav_chart_{timestamp}.png")
  1432. plt.savefig(chart_filename, dpi=300)
  1433. plt.close()
  1434. logger.info(f"AVA_VAV chart saved to: {chart_filename}")
  1435. return chart_filename
  1436. except Exception as e:
  1437. logger.error(f"Failed to generate AVA_VAV chart: {str(e)}", exc_info=True)
  1438. return None
  1439. def generate_msdv_chart(comfort_calculator, output_dir: str) -> Optional[str]:
  1440. """
  1441. Generate MSDV (Motion Sickness Dose Value) metric chart with data saved to CSV first.
  1442. This version first saves data to CSV, then uses the CSV to generate the chart.
  1443. Args:
  1444. comfort_calculator: ComfortCalculator instance
  1445. output_dir: Output directory
  1446. Returns:
  1447. str: Chart file path, or None if generation fails
  1448. """
  1449. logger = LogManager().get_logger()
  1450. try:
  1451. # 获取数据
  1452. df = comfort_calculator.ego_df.copy()
  1453. msdv_value = comfort_calculator.calculated_value.get('msdv', 0)
  1454. motion_sickness_prob = comfort_calculator.calculated_value.get('motionSickness', 0)
  1455. if df.empty:
  1456. logger.warning("Cannot generate MSDV chart: empty data")
  1457. return None
  1458. # 确保有必要的列
  1459. if 'accelX' not in df.columns or 'accelY' not in df.columns:
  1460. logger.warning("Missing required columns for MSDV chart")
  1461. return None
  1462. # 获取阈值
  1463. thresholds = get_metric_thresholds(comfort_calculator, 'msdv')
  1464. min_threshold = thresholds.get('min')
  1465. max_threshold = thresholds.get('max')
  1466. # 将东北天坐标系下的加速度转换为车身坐标系下的加速度
  1467. if 'posH' not in df.columns:
  1468. logger.warning("Missing heading angle data for coordinate transformation")
  1469. return None
  1470. # 车身坐标系:X轴指向车头,Y轴指向车辆左侧,Z轴指向车顶
  1471. df['posH_rad'] = np.radians(df['posH'])
  1472. # 转换加速度到车身坐标系
  1473. df['a_x_body'] = df['accelX'] * np.sin(df['posH_rad']) + df['accelY'] * np.cos(df['posH_rad'])
  1474. df['a_y_body'] = df['accelX'] * np.cos(df['posH_rad']) - df['accelY'] * np.sin(df['posH_rad'])
  1475. df['a_z_body'] = df['accelZ'] if 'accelZ' in df.columns else pd.Series(np.zeros(len(df)))
  1476. # 生成时间戳
  1477. import datetime
  1478. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  1479. # 保存 CSV 数据(第一步)
  1480. csv_filename = os.path.join(output_dir, f"msdv_data_{timestamp}.csv")
  1481. df_csv = pd.DataFrame({
  1482. 'simTime': df['simTime'],
  1483. 'a_x_body': df['a_x_body'],
  1484. 'a_y_body': df['a_y_body'],
  1485. 'a_z_body': df['a_z_body'],
  1486. 'v': df['v'],
  1487. 'min_threshold': min_threshold,
  1488. 'max_threshold': max_threshold,
  1489. 'msdv_value': msdv_value,
  1490. 'motion_sickness_prob': motion_sickness_prob
  1491. })
  1492. df_csv.to_csv(csv_filename, index=False)
  1493. logger.info(f"MSDV data saved to: {csv_filename}")
  1494. # 第二步:从 CSV 读取(可验证保存数据无误)
  1495. df = pd.read_csv(csv_filename)
  1496. # 创建图表(第三步)
  1497. plt.figure(figsize=(12, 8))
  1498. # 绘制三轴加速度
  1499. plt.subplot(3, 1, 1)
  1500. plt.plot(df['simTime'], df['a_x_body'], 'r-', label='X-axis Acceleration')
  1501. # 添加阈值线
  1502. if 'min_threshold' in df.columns and df['min_threshold'].iloc[0] is not None:
  1503. min_threshold = df['min_threshold'].iloc[0]
  1504. plt.axhline(y=min_threshold, color='r', linestyle=':', label=f'Min Threshold ({min_threshold})')
  1505. if 'max_threshold' in df.columns and df['max_threshold'].iloc[0] is not None:
  1506. max_threshold = df['max_threshold'].iloc[0]
  1507. plt.axhline(y=max_threshold, color='g', linestyle=':', label=f'Max Threshold ({max_threshold})')
  1508. plt.xlabel('Time (s)')
  1509. plt.ylabel('Acceleration (m/s²)')
  1510. plt.title('Body X-axis Acceleration (Longitudinal)')
  1511. plt.grid(True)
  1512. plt.legend()
  1513. plt.subplot(3, 1, 2)
  1514. plt.plot(df['simTime'], df['a_y_body'], 'g-', label='Y-axis Acceleration')
  1515. plt.xlabel('Time (s)')
  1516. plt.ylabel('Acceleration (m/s²)')
  1517. plt.title('Body Y-axis Acceleration (Lateral)')
  1518. plt.grid(True)
  1519. plt.legend()
  1520. plt.subplot(3, 1, 3)
  1521. plt.plot(df['simTime'], df['a_z_body'], 'b-', label='Z-axis Acceleration')
  1522. plt.xlabel('Time (s)')
  1523. plt.ylabel('Acceleration (m/s²)')
  1524. msdv_value = df['msdv_value'].iloc[0] if 'msdv_value' in df.columns else 0
  1525. motion_sickness_prob = df['motion_sickness_prob'].iloc[0] if 'motion_sickness_prob' in df.columns else 0
  1526. plt.title(f'Body Z-axis Acceleration (Vertical) - MSDV: {msdv_value:.4f}, Motion Sickness Probability: {motion_sickness_prob:.2f}%')
  1527. plt.grid(True)
  1528. plt.legend()
  1529. plt.tight_layout()
  1530. # 保存图像
  1531. chart_filename = os.path.join(output_dir, f"msdv_chart_{timestamp}.png")
  1532. plt.savefig(chart_filename, dpi=300)
  1533. plt.close()
  1534. logger.info(f"MSDV chart saved to: {chart_filename}")
  1535. return chart_filename
  1536. except Exception as e:
  1537. logger.error(f"Failed to generate MSDV chart: {str(e)}", exc_info=True)
  1538. return None
  1539. def generate_traffic_chart_data(traffic_calculator, metric_name: str, output_dir: Optional[str] = None) -> Optional[str]:
  1540. """Generate chart data for traffic metrics"""
  1541. # 待实现
  1542. return None
  1543. def generate_function_chart_data(function_calculator, metric_name: str, output_dir: Optional[str] = None) -> Optional[str]:
  1544. """Generate chart data for function metrics"""
  1545. # 待实现
  1546. return None