v0.1.17; update 7-9

This commit is contained in:
wty-yy
2025-12-27 16:13:18 +00:00
parent f92f34ac14
commit baf11a6cd4
12 changed files with 405 additions and 114 deletions

1
.gitignore vendored
View File

@@ -8,5 +8,6 @@ __pycache__/
# Logger # Logger
logs/ logs/
logs*/
.aim/ .aim/
*.bkp *.bkp

18
CMD.md
View File

@@ -51,9 +51,8 @@ python robogauge/scripts/run.py \
--task go2_moe \ --task go2_moe \
--experiment-name debug \ --experiment-name debug \
--stress-benchmark \ --stress-benchmark \
--stress-terrain-names flat slope stairs_up stairs_down wave \ --stress-terrain-names flat slope stairs_up stairs_down wave obstacle \
--stress-num-processes 2 \ --num-processes 6 \
--num-processes 3 \
--seeds 0 1 2 3 4 \ --seeds 0 1 2 3 4 \
--frictions 0.5 1.0 1.5 2.0 2.5 \ --frictions 0.5 1.0 1.5 2.0 2.5 \
--compress-logs \ --compress-logs \
@@ -64,14 +63,23 @@ python robogauge/scripts/run.py \
将Multi Run结果绘制在雷达图中 将Multi Run结果绘制在雷达图中
```bash ```bash
# 可选--out保存到图片 # 可选--out保存到图片
python robogauge/utils/visualize_results.py \ python robogauge/utils/visualize/plot_radar_and_bar.py \
aggregated_results_1.yaml \ aggregated_results_1.yaml \
aggregated_results_2.yaml \ aggregated_results_2.yaml \
--out logs/1.jpg --out logs/1.jpg
# 绘制下全部 *.yaml, 可选range控制radar, bar y轴显示范围 # 绘制下全部 *.yaml, 可选range控制radar, bar y轴显示范围
python robogauge/utils/visualize_results.py \ python robogauge/utils/visualize/plot_radar_and_bar.py \
/home/xfy/Coding/robot_gauge/mytest/results \ /home/xfy/Coding/robot_gauge/mytest/results \
--range 0.35 1.0 \ --range 0.35 1.0 \
--out logs/1.jpg --out logs/1.jpg
``` ```
# Terrain Levels Plot
```bash
# 绘制5种可变等级的地形与摩擦系数的关系图
python robogauge/utils/visualize/plot_terrain_levels.py \
aggregated_results_1.yaml \
aggregated_results_2.yaml \
--out logs/terrain_analysis.jpg
```

View File

@@ -13,10 +13,11 @@
| Unitree go2 | 四足 | ✅ | | Unitree go2 | 四足 | ✅ |
## 安装 ## 安装
参考[PyTorch官网](https://pytorch.org/get-started/locally/)安装2.0以上任意版本, 安装本仓库 推荐安装`python=3.8`, 参考[PyTorch官网](https://pytorch.org/get-started/locally/)安装2.0以上任意版本, 安装本仓库
```bash ```bash
git clone https://github.com/wty-yy/RoboGauge.git git clone https://github.com/wty-yy/RoboGauge.git
cd RoboGauge
pip install -e . pip install -e .
``` ```

View File

@@ -6,7 +6,10 @@
3. 在StressPipeline测试中, 不再保存 3. 在StressPipeline测试中, 不再保存
4. 添加obstacles地形 4. 添加obstacles地形
5. 优化分数计算方法, 按照`(level-1)*0.1+metric_score`计算 5. 优化分数计算方法, 按照`(level-1)*0.1+metric_score`计算
6. 加入日志文件夹递归保存, StressPipeline -> LevelPipeline -> MultiPipeline, 加入压缩参数`--compress-logs`, 将日志文件夹压缩为`subtasks.tar.xz`, 日志文件压缩`4M->100kb` 6. 加入日志文件夹递归保存, StressPipeline -> LevelPipeline -> MultiPipeline, 加入压缩参数`--compress-logs`, 将日志文件夹压缩为`subtasks.tar.xz`, 日志文件压缩`4M->100kb` (6种地形的全部评估测试日志压缩后`29M->539K`)
7. StressPipeline和MultiPipeline同时工作时, 很大概率发生死锁 (StressPipeline传入的任务超过10个以上), 将MultiPipeline的num-processes设置为1即可避免该问题 (不创建Process), 为了提高速度可以将flat的不同域随机化也展平
8. 将进度条换成当前时间/剩余时间
9. 添加`plot_terrain_levels.py`绘图代码, 绘制5种可变等级的地形与摩擦系数的关系图
## 20251226 ## 20251226
### v0.1.16 ### v0.1.16
1. 基本完成StressPipeline, 加入绘制进度条的线程, 其他进程通过Queue更新主进程的进度条 1. 基本完成StressPipeline, 加入绘制进度条的线程, 其他进程通过Queue更新主进程的进度条

View File

@@ -91,17 +91,13 @@ class MultiPipeline:
multi_logger.info(f"🔢 Seeds: {self.seeds}, Frictions: {self.frictions}, Base masses: {self.base_masses}") multi_logger.info(f"🔢 Seeds: {self.seeds}, Frictions: {self.frictions}, Base masses: {self.base_masses}")
workers_data = list(product(self.seeds, self.base_masses, self.frictions)) workers_data = list(product(self.seeds, self.base_masses, self.frictions))
report_progress(self.progress_data, ProgressTypes.INIT, total=len(workers_data), desc="🚀 MultiPipeline Executing") report_progress(self.progress_data, ProgressTypes.INIT, total=len(workers_data), desc="🚀 MultiPipeline")
ctx = multiprocessing.get_context('spawn') ctx = multiprocessing.get_context('spawn')
worker_func = functools.partial(run_single_process, self.args) worker_func = functools.partial(run_single_process, self.args)
results_list = [] results_list = []
with NoDaemonPool(processes=self.num_processes, context=ctx) as pool:
iterator = pool.imap_unordered(worker_func, workers_data) def update_results(results):
bar = iterator
if self.console_output:
bar = tqdm(iterator, total=len(workers_data), desc="Evaluation")
for results in bar:
results_list.append(results) results_list.append(results)
self.add_static_info('model_path', results['model_path']) self.add_static_info('model_path', results['model_path'])
self.add_static_info('terrain_name', results['results']['terrain_name']) self.add_static_info('terrain_name', results['results']['terrain_name'])
@@ -111,6 +107,23 @@ class MultiPipeline:
data = results['data'] data = results['data']
multi_logger.error(f"❌ Process with seed={data[0]}, base_mass={data[1]}, friction={data[2]} failed with error: {results['error_msg']}") multi_logger.error(f"❌ Process with seed={data[0]}, base_mass={data[1]}, friction={data[2]} failed with error: {results['error_msg']}")
if self.num_processes == 1:
multi_logger.info("🚀 Running in Serial Mode")
bar = workers_data
if self.console_output:
bar = tqdm(workers_data, desc="Evaluation")
for data in bar:
results = worker_func(data)
update_results(results)
else:
with NoDaemonPool(processes=self.num_processes, context=ctx) as pool:
iterator = pool.imap_unordered(worker_func, workers_data)
bar = iterator
if self.console_output:
bar = tqdm(iterator, total=len(workers_data), desc="Evaluation")
for results in bar:
update_results(results)
multi_logger.info("✅ Multi-Process Evaluation Completed.") multi_logger.info("✅ Multi-Process Evaluation Completed.")
aggregated_results = self.aggregate_results(results_list) aggregated_results = self.aggregate_results(results_list)
return aggregated_results return aggregated_results

View File

@@ -41,23 +41,22 @@ def run_pipeline(args, progress_queue, data):
args = deepcopy(args) args = deepcopy(args)
task_id = data['task_id'] task_id = data['task_id']
search = data['search_max_level'] search = data['search_max_level']
task_label = f"[{data['terrain_name']}]" task_label = f"[{data['terrain_name']}] M:{data['base_mass']} F:{data['friction']}"
progress_data = ProgressData( progress_data = ProgressData(
task_id=task_id, task_id=task_id,
msg_prefix=task_label + ' ', msg_prefix=task_label + ' ',
progress_queue=progress_queue progress_queue=progress_queue
) )
if search is True:
task_label += f" M:{data['base_mass']} F:{data['friction']}"
progress_data.msg_prefix = task_label + ' '
args.friction = data['friction'] args.friction = data['friction']
args.frictions = [data['friction']] args.frictions = [data['friction']]
args.base_mass = data['base_mass'] args.base_mass = data['base_mass']
args.base_masses = [data['base_mass']] args.base_masses = [data['base_mass']]
args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}" args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}"
args.experiment_name = f"{args.experiment_name}_{data['terrain_name']}_M{data['base_mass']}_F{data['friction']}" args.experiment_name = f"{args.experiment_name}_{data['terrain_name']}_M{data['base_mass']}_F{data['friction']}"
args.goals = GOALS['level_pipeline']
if search is True:
args.goals = GOALS['level_pipeline']
level, level_results = LevelPipeline(args, console_output=False, progress_data=progress_data).run() level, level_results = LevelPipeline(args, console_output=False, progress_data=progress_data).run()
if level == 0: # no valid level found if level == 0: # no valid level found
report_progress(progress_data, ProgressTypes.FINISH, desc=f"❌ Failed (Lv 0)") report_progress(progress_data, ProgressTypes.FINISH, desc=f"❌ Failed (Lv 0)")
@@ -68,8 +67,8 @@ def run_pipeline(args, progress_queue, data):
'level': 0, 'level': 0,
} }
return results return results
report_progress(progress_data, ProgressTypes.RESET, total=0, desc=f"✅ Found Lv {level} -> Running") report_progress(progress_data, ProgressTypes.RESET, total=0, desc=f"✅ Found Lv {level} -> Running")
progress_data.msg_prefix += f"(Lv {level}) "
else: else:
level = None # flat terrain level = None # flat terrain
args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}" args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}"
@@ -91,13 +90,14 @@ class StressPipeline:
self.args = args self.args = args
self.seeds = args.seeds self.seeds = args.seeds
self.task_robot_model = args.task_name.split('.')[0] self.task_robot_model = args.task_name.split('.')[0]
self.num_processes = args.stress_num_processes self.num_processes = args.num_processes
args.experiment_name = self.task_robot_model + '_stress' + ('' if args.cli_experiment_name is None else '_' + args.cli_experiment_name) args.experiment_name = self.task_robot_model + '_stress' + ('' if args.cli_experiment_name is None else '_' + args.cli_experiment_name)
self.static_info = {} self.static_info = {}
stress_logger.create(args.experiment_name, args.run_name) stress_logger.create(args.experiment_name, args.run_name)
self.args.parent_log_dir = str(stress_logger.log_dir / "subtasks") self.args.parent_log_dir = str(stress_logger.log_dir / "subtasks")
self.compress_logs = args.compress_logs self.compress_logs = args.compress_logs
args.compress_logs = False # Disable child log compression args.compress_logs = False # Disable child log compression
args.num_processes = 1 # Disable child multi-process
def add_static_info(self, key: str, value): def add_static_info(self, key: str, value):
if key not in self.static_info: if key not in self.static_info:
@@ -125,7 +125,6 @@ class StressPipeline:
'terrain_name': terrain_name, 'terrain_name': terrain_name,
'search_max_level': search_max_level, 'search_max_level': search_max_level,
} }
if search_max_level:
for friction, base_mass in product(self.args.frictions, self.args.base_masses): for friction, base_mass in product(self.args.frictions, self.args.base_masses):
now_data = deepcopy(data) now_data = deepcopy(data)
now_data.update({ now_data.update({
@@ -133,8 +132,6 @@ class StressPipeline:
'base_mass': base_mass, 'base_mass': base_mass,
}) })
workers_data.append(now_data) workers_data.append(now_data)
else:
workers_data.append(data)
### Start progress monitor ### ### Start progress monitor ###
progress_queue, monitor_thread = start_progress_monitor_thread(len(workers_data)) progress_queue, monitor_thread = start_progress_monitor_thread(len(workers_data))
@@ -182,15 +179,13 @@ class StressPipeline:
stress_logger.error("No results to aggregate.") stress_logger.error("No results to aggregate.")
return return
summary = {**self.static_info, 'summary': {}} summary = {**self.static_info, 'summary': {}, 'final_score': {}}
value_collections = defaultdict(lambda: defaultdict(list)) value_collections = defaultdict(lambda: defaultdict(list))
zero_terrain_count = 0 zero_terrain_count = 0
for result in all_results: for result in all_results:
terrain_name = result['data']['terrain_name'] terrain_name = result['data']['terrain_name']
terrain_level = result['level'] # None, 0, 1, ..., 10 terrain_level = result['level'] # None, 0, 1, ..., 10
key = terrain_name key = f'{terrain_name}_{terrain_level}'
if terrain_level is not None:
key += f'_{terrain_level}'
key += f'_baseMass{result["data"]["base_mass"]}_friction{result["data"]["friction"]}' key += f'_baseMass{result["data"]["base_mass"]}_friction{result["data"]["friction"]}'
if terrain_level == 0: if terrain_level == 0:
summary[key] = None summary[key] = None
@@ -201,14 +196,19 @@ class StressPipeline:
for metric, means in result['results']['summary'].items(): for metric, means in result['results']['summary'].items():
for mean_name, mean_value in means.items(): for mean_name, mean_value in means.items():
value = float(mean_value.split(' ')[0]) value = float(mean_value.split(' ')[0])
value += (terrain_level - 1) * 0.1 if terrain_level is not None else 0.0 if terrain_level is not None: # level terrain
value = (terrain_level - 1) * 0.1 + value * 0.1
value_collections[metric][mean_name].append(value) value_collections[metric][mean_name].append(value)
final_metrics = defaultdict(list)
for metric, means in value_collections.items(): for metric, means in value_collections.items():
summary['summary'][metric] = {} summary['summary'][metric] = {}
for mean_name, values in means.items(): for mean_name, values in means.items():
values.extend([0.0] * zero_terrain_count) # include zero terrains values.extend([0.0] * zero_terrain_count) # include zero terrains
summary['summary'][metric][mean_name] = f"{float(np.mean(values)):.4f} ± {float(np.std(values)):.4f}" summary['summary'][metric][mean_name] = f"{float(np.mean(values)):.4f} ± {float(np.std(values)):.4f}"
final_metrics[mean_name].append(np.mean(values))
for mean_name, values in final_metrics.items():
summary['final_score'][mean_name] = float(np.mean(values))
save_path = stress_logger.log_dir / "stress_benchmark_results.yaml" save_path = stress_logger.log_dir / "stress_benchmark_results.yaml"
with open(save_path, 'w') as file: with open(save_path, 'w') as file:

View File

@@ -80,7 +80,6 @@ def parse_args():
# Multiprocessing parameters, with different seeds # Multiprocessing parameters, with different seeds
{"name": "--multi", "action": "store_true", "default": False, "help": "Enable multiprocessing."}, {"name": "--multi", "action": "store_true", "default": False, "help": "Enable multiprocessing."},
{"name": "--num-processes", "type": int, "default": 2, "help": "Number of parallel processes."},
{"name": "--seeds", "type": int, "nargs": "+", "default": [0, 1, 2, 3, 4], "help": "List of random seeds for multiple runs."}, {"name": "--seeds", "type": int, "nargs": "+", "default": [0, 1, 2, 3, 4], "help": "List of random seeds for multiple runs."},
{"name": "--base-masses", "type": float, "nargs": "+", "default": [0], "help": "List of base masses for the model."}, {"name": "--base-masses", "type": float, "nargs": "+", "default": [0], "help": "List of base masses for the model."},
{"name": "--frictions", "type": float, "nargs": "+", "default": [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5], "help": "List of friction coefficients for the model."}, {"name": "--frictions", "type": float, "nargs": "+", "default": [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5], "help": "List of friction coefficients for the model."},
@@ -91,8 +90,9 @@ def parse_args():
# Stress pipeline parameters # Stress pipeline parameters
{"name": "--stress-benchmark", "action": "store_true", "default": False, "help": "Use stress pipeline to benchmark model robustness."}, {"name": "--stress-benchmark", "action": "store_true", "default": False, "help": "Use stress pipeline to benchmark model robustness."},
{"name": "--stress-terrain-names", "type": str, "nargs": "+", "default": ["flat", "slope", "wave", "stairs_up", "stairs_down"], "help": "List of terrain names for stress benchmark."}, {"name": "--stress-terrain-names", "type": str, "nargs": "+", "default": ["flat", "slope", "wave", "stairs_up", "stairs_down"], "help": "List of terrain names for stress benchmark."},
{"name": "--stress-num-processes", "type": int, "default": 2, "help": "Number of parallel processes for stress benchmark."},
# Common parameters
{"name": "--num-processes", "type": int, "default": 2, "help": "Number of parallel processes for Multi or Stress benchmark."},
{"name": "--compress-logs", "action": "store_true", "default": False, "help": "Compress and delete logs after run."}, {"name": "--compress-logs", "action": "store_true", "default": False, "help": "Compress and delete logs after run."},
] ]
for param in parameters: for param in parameters:

View File

@@ -2,16 +2,17 @@
''' '''
@File : progress_monitor.py @File : progress_monitor.py
@Time : 2025/12/27 00:10:07 @Time : 2025/12/27 00:10:07
@Author : wty-yy @Author : wty-yy (with Gemini 3)
@Version : 1.0 @Version : 1.0
@Blog : https://wty-yy.github.io/ @Blog : https://wty-yy.github.io/
@Desc : Centralized progress monitoring for multiprocessing tasks using tqdm @Desc : Centralized progress monitoring with Dynamic Slot Management
''' '''
from tqdm import tqdm from tqdm import tqdm
import multiprocessing import multiprocessing
from typing import Dict from typing import Dict
from threading import Thread from threading import Thread
from dataclasses import dataclass from dataclasses import dataclass
import heapq
class ProgressTypes: class ProgressTypes:
INIT = 'init' # Init progress (set total, desc) INIT = 'init' # Init progress (set total, desc)
@@ -50,76 +51,111 @@ def report_progress(progress_data: ProgressData, msg_type, value=None, desc=None
class ProgressMonitor: class ProgressMonitor:
def __init__(self, total_rows): def __init__(self, total_rows):
self.total_rows = total_rows self.total_rows = total_rows
self.bars: Dict[int, tqdm] = {} self.active_bars: Dict[int, Dict] = {}
self.free_slots = []
self.max_slot_used = 0
def _get_free_slot(self):
""" Get a free screen display position """
if self.free_slots:
return heapq.heappop(self.free_slots)
else:
self.max_slot_used += 1
return self.max_slot_used
def _release_slot(self, slot):
""" Release a screen display position """
heapq.heappush(self.free_slots, slot)
def listener_loop(self, queue): def listener_loop(self, queue):
""" """
Run in a separate thread in the main process to consume the Queue and update tqdm. Listener thread in the main process
""" """
# print(f"Monitor started for {self.total_rows} tasks...") # Create the main progress bar (Position 0)
for i in range(self.total_rows): main_bar = tqdm(
self.bars[i] = tqdm( total=self.total_rows,
total=100, position=0,
position=i, desc="🚀 Total Progress",
desc=f"Task {i} Pending...", bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [Done: {n_fmt}] [{elapsed}<{remaining}]",
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]", leave=True,
leave=True smoothing=0.1 # Optional: set smoothing factor (0-1) to prevent drastic ETA fluctuations due to slow tasks
) )
active_tasks = self.total_rows # Number of tasks still pending
pending_tasks = self.total_rows
while active_tasks > 0: while pending_tasks > 0:
record = queue.get() record = queue.get()
if record is None: # Poison pill signal if record is None: break
break
task_id, msg_type, data = record task_id, msg_type, data = record
if task_id not in self.bars:
continue
bar = self.bars[task_id] # --- Dynamically create/get sub progress bars ---
if task_id not in self.active_bars:
# Only create bar when message received to avoid Pending screen flicker
slot = self._get_free_slot()
# leave=False is key: after task completion, clear the line for new task reuse
bar = tqdm(
total=100,
position=slot,
desc=f"Task {task_id} Starting...",
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
leave=False
)
self.active_bars[task_id] = {'bar': bar, 'slot': slot}
bar_info = self.active_bars[task_id]
bar = bar_info['bar']
# --- Handle messages ---
try:
if msg_type == ProgressTypes.INIT: if msg_type == ProgressTypes.INIT:
total = data['total'] total = data.get('total', 100)
desc = data['desc'] desc = data.get('desc', '')
bar.reset(total=total) bar.reset(total=total)
bar.set_description(desc) bar.set_description(desc)
bar.refresh() bar.refresh()
elif msg_type == ProgressTypes.UPDATE: elif msg_type == ProgressTypes.UPDATE:
val = data['value'] val = data.get('value', 1)
bar.update(val) bar.update(val)
elif msg_type == ProgressTypes.DESC: elif msg_type == ProgressTypes.DESC:
desc = data['desc'] desc = data.get('desc')
if desc: if desc: bar.set_description(desc)
bar.set_description(desc)
elif msg_type == ProgressTypes.RESET: elif msg_type == ProgressTypes.RESET:
# Scenario: Level search finished, starting MultiPipeline, reset progress bar total = data.get('total', 100)
total = data['total'] desc = data.get('desc', '')
desc = data['desc']
bar.reset(total=total) bar.reset(total=total)
bar.set_description(desc) bar.set_description(desc)
bar.refresh() bar.refresh()
elif msg_type == ProgressTypes.FINISH: elif msg_type == ProgressTypes.FINISH or msg_type == ProgressTypes.ERROR:
desc = data['desc'] # Update sub progress bar status
if desc: desc = data.get('desc')
bar.set_description(desc) if desc: bar.set_description(desc)
bar.refresh() bar.refresh()
# Note: We do not close the bar here to keep it displayed until all tasks are finished and closed together.
active_tasks -= 1
elif msg_type == ProgressTypes.ERROR: # Close sub progress bar and release slot
desc = data['desc'] bar.close() # 因为 leave=False这行会从屏幕消失
bar.set_description(desc) slot = bar_info['slot']
bar.refresh() self._release_slot(slot)
active_tasks -= 1 del self.active_bars[task_id]
# After all tasks are finished, close all bars # Update the main progress bar
for bar in self.bars.values(): main_bar.update(1)
bar.close() main_bar.refresh()
pending_tasks -= 1
except Exception as e:
pass # Prevent printing errors from disrupting layout
# Close all remaining bars (theoretically should be empty except main_bar)
main_bar.close()
for info in self.active_bars.values():
info['bar'].close()
def start_progress_monitor_thread(total_rows): def start_progress_monitor_thread(total_rows):
""" """

View File

@@ -1,3 +1,12 @@
# -*- coding: utf-8 -*-
'''
@File : plot_radar_and_bar.py
@Time : 2025/12/27 22:52:24
@Author : wty-yy (with Gemini 3)
@Version : 1.0
@Blog : https://wty-yy.github.io/
@Desc : 可视化评测结果生成雷达图和柱状图
'''
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
@@ -48,9 +57,10 @@ def load_data(file_paths):
model_name = os.path.basename(raw_path).replace('.pt', '') model_name = os.path.basename(raw_path).replace('.pt', '')
values = [] values = []
summary = content['summary']
for k in metric_keys: for k in metric_keys:
if k in content: if k in summary:
raw_val = content[k]['mean@50'] raw_val = summary[k]['mean@25']
values.append(parse_value_string(raw_val)) values.append(parse_value_string(raw_val))
else: else:
values.append(0.0) values.append(0.0)

View File

@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
'''
@File : plot_terrain_levels.py
@Time : 2025/12/27 23:04:01
@Author : wty-yy (with Gemini 3)
@Version : 1.0
@Blog : https://wty-yy.github.io/
@Desc : None
@Desc : 可视化不同地形下随着摩擦力变化的关卡通过等级 (Terrain Level vs Friction)
'''
import matplotlib.pyplot as plt
from pathlib import Path
import numpy as np
import yaml
import argparse
import os
import re
# --- 配置 Matplotlib 样式 ---
config = {
"font.family": 'serif',
"figure.figsize": (18, 10),
"font.size": 12,
"mathtext.fontset": 'cm',
'axes.unicode_minus': False
}
plt.rcParams.update(config)
plt.rcParams['font.serif'] = ['Times New Roman', 'DejaVu Serif', 'serif']
def load_terrain_data(file_paths):
"""
加载数据并按模型、地形、摩擦力组织
Returns:
model_data: {model_name: {terrain_type: {friction: level}}}
"""
target_terrains = ['wave', 'slope', 'stairs_up', 'stairs_down', 'obstacle']
model_data = {}
# 用于从键名中提取 friction 的正则 (例如 friction1.25)
friction_pattern = re.compile(r'friction([\d\.]+)')
for path in file_paths:
if not os.path.exists(path):
print(f"[ERROR] File not found: {path}")
continue
with open(path, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f)
# 获取模型名称
raw_path = content.get('model_path', 'Unknown_Model')
model_name = os.path.basename(raw_path).replace('.pt', '')
if model_name not in model_data:
model_data[model_name] = {t: {} for t in target_terrains}
# 遍历YAML中的每个测试条目
for key, value in content.items():
if key in ['model_path', 'summary']:
continue
# 1. 检查地形是否在目标列表中
t_name = value.get('terrain_name')
if t_name not in target_terrains:
continue
# 2. 从 key 中提取摩擦力
match = friction_pattern.search(key)
if not match:
continue
friction = float(match.group(1))
# 3. 获取地形等级 (取该条件下该地形的最大值,防止重复)
level = value.get('terrain_level', 0)
# 记录数据
current_max = model_data[model_name][t_name].get(friction, -1)
if level > current_max:
model_data[model_name][t_name][friction] = level
return model_data, target_terrains
def plot_results(model_data, terrain_labels, output_file=None):
"""绘制 2x3 的子图"""
if not model_data:
print("No data to plot.")
return
# 设置 2行3列 的布局
fig, axes = plt.subplots(2, 3, figsize=(18, 10), sharex=True, sharey=True)
axes = axes.flatten()
# 自动获取颜色映射
colors = plt.cm.get_cmap("tab10", len(model_data))
# 遍历前5个地形进行绘图
for i, t_name in enumerate(terrain_labels):
ax = axes[i]
for idx, (model_name, terrains) in enumerate(model_data.items()):
data_points = terrains.get(t_name, {})
if not data_points:
continue
# 排序 (Friction, Level)
sorted_points = sorted(data_points.items())
xs, ys = zip(*sorted_points)
ax.plot(xs, ys, marker='o', linestyle='-', linewidth=2,
label=model_name, color=colors(idx), alpha=0.8)
ax.set_title(t_name.replace('_', ' ').title(), fontsize=14)
ax.grid(True, linestyle='--', alpha=0.6)
# --- 第6张图绘制平均值 (Average) ---
ax_avg = axes[5]
for idx, (model_name, terrains) in enumerate(model_data.items()):
# 聚合该模型下所有地形的数据用于计算平均值
# 结构: friction -> [level_slope, level_wave, ...]
friction_agg = {}
for t_name in terrain_labels:
for f, l in terrains[t_name].items():
if f not in friction_agg:
friction_agg[f] = []
friction_agg[f].append(l)
if not friction_agg:
continue
# 计算平均值并排序
sorted_frictions = sorted(friction_agg.keys())
avg_levels = [np.mean(friction_agg[f]) for f in sorted_frictions]
ax_avg.plot(sorted_frictions, avg_levels, marker='s', linestyle='-', linewidth=2,
label=model_name, color=colors(idx), alpha=0.8)
ax_avg.set_title('Average Performance', fontsize=14)
ax_avg.grid(True, linestyle='--', alpha=0.6)
# 仅在最后一张图显示图例,避免遮挡
ax_avg.legend(loc='best', frameon=False, title="Models")
# 最下面一行设置 X 轴标签
for i in [3, 4, 5]:
axes[i].set_xlabel('Friction ($\mu$)')
# 最左边一列设置 Y 轴标签
for i in [0, 3]:
axes[i].set_ylabel('Terrain Level')
plt.tight_layout()
# 稍微调整间距防止标签重叠
# plt.subplots_adjust(wspace=0.1, hspace=0.15)
if output_file:
plt.savefig(output_file, dpi=300, bbox_inches='tight')
print(f"Plot saved to {output_file}")
else:
plt.show()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Plot terrain level vs friction curves.")
parser.add_argument('files', metavar='F', type=str, nargs='+', help='YAML result files')
parser.add_argument('--out', type=str, default='terrain_analysis.jpg', help='Output image filename')
args = parser.parse_args()
data, labels = load_terrain_data(args.files)
plot_results(data, labels, output_file=args.out)

49
run.bash Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
source /root/Programs/miniforge3/bin/activate robot
python robogauge/scripts/run.py \
--task go2_moe \
--experiment-name debug \
--stress-benchmark \
--stress-terrain-names flat slope stairs_up stairs_down wave obstacle \
--num-processes 62 \
--seeds 0 1 2 3 4 \
--frictions 0.75 1.0 1.25 1.5 1.75 2.0 2.25 2.5 \
--compress-logs \
--headless
python robogauge/scripts/run.py \
--task go2_moe \
--model-path /root/Coding/RoboGauge/mytest/go2_moe_cts_hard_terrain_141k.pt \
--experiment-name debug \
--stress-benchmark \
--stress-terrain-names flat slope stairs_up stairs_down wave obstacle \
--num-processes 62 \
--seeds 0 1 2 3 4 \
--frictions 0.75 1.0 1.25 1.5 1.75 2.0 2.25 2.5 \
--compress-logs \
--headless
python robogauge/scripts/run.py \
--task go2 \
--model-path /root/Coding/RoboGauge/mytest/go2_cts_hard_terrain_141k.pt \
--experiment-name debug \
--stress-benchmark \
--stress-terrain-names flat slope stairs_up stairs_down wave obstacle \
--num-processes 62 \
--seeds 0 1 2 3 4 \
--frictions 0.75 1.0 1.25 1.5 1.75 2.0 2.25 2.5 \
--compress-logs \
--headless
python robogauge/scripts/run.py \
--task go2 \
--experiment-name debug \
--stress-benchmark \
--stress-terrain-names flat slope stairs_up stairs_down wave obstacle \
--num-processes 62 \
--seeds 0 1 2 3 4 \
--frictions 0.75 1.0 1.25 1.5 1.75 2.0 2.25 2.5 \
--compress-logs \
--headless

View File

@@ -13,9 +13,11 @@ setup(
"mujoco>=3.0.0", "mujoco>=3.0.0",
"dm_control>=1.0.14", "dm_control>=1.0.14",
"scipy", "scipy",
"matplotlib", "matplotlib==3.6.3",
"tqdm", "tqdm",
"imageio[ffmpeg]", "imageio[ffmpeg]",
"tensorboard",
"PyYAML",
], ],
python_requires=">=3.8", python_requires=">=3.8",