From 65490fe591f03360439b0d4222eb44e1547f119a Mon Sep 17 00:00:00 2001 From: wty-yy <993660140@qq.com> Date: Fri, 26 Dec 2025 14:47:39 +0800 Subject: [PATCH] v0.1.16; Add StressPipeline --- .vscode/launch.json | 10 +- CMD.md | 30 ++-- UPDATE.md | 5 + resources/terrains/slope/slope_1.xml | 2 +- resources/terrains/slope/slope_10.xml | 2 +- resources/terrains/slope/slope_2.xml | 2 +- resources/terrains/slope/slope_3.xml | 2 +- resources/terrains/slope/slope_4.xml | 2 +- resources/terrains/slope/slope_5.xml | 2 +- resources/terrains/slope/slope_6.xml | 2 +- resources/terrains/slope/slope_7.xml | 2 +- resources/terrains/slope/slope_8.xml | 2 +- resources/terrains/slope/slope_9.xml | 2 +- robogauge/scripts/run_eval_models.sh | 6 +- robogauge/tasks/__init__.py | 27 ++-- robogauge/tasks/custom/go2/__init__.py | 2 +- robogauge/tasks/custom/go2/go2_slope_task.py | 5 - robogauge/tasks/gauge/base_gauge_config.py | 3 +- .../gauge/gauge_configs/slope_gauge_config.py | 9 +- .../gauge_configs/terrain_levels_config.py | 58 +++++--- robogauge/tasks/pipeline/__init__.py | 5 +- robogauge/tasks/pipeline/level_pipeline.py | 24 ++-- robogauge/tasks/pipeline/multi_pipeline.py | 47 ++++--- robogauge/tasks/pipeline/stress_pipeline.py | 131 ++++++++++++++++++ robogauge/tasks/robots/__init__.py | 4 +- robogauge/tasks/robots/go2/go2_config.py | 8 +- robogauge/tasks/robots/go2/go2_moe_config.py | 7 + robogauge/tasks/simulator/mujoco_config.py | 4 +- robogauge/tasks/simulator/mujoco_simulator.py | 16 ++- robogauge/utils/helpers.py | 15 +- robogauge/utils/task_register.py | 4 +- 31 files changed, 318 insertions(+), 122 deletions(-) create mode 100644 robogauge/tasks/pipeline/stress_pipeline.py diff --git a/.vscode/launch.json b/.vscode/launch.json index a47b4fc..2205dea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "program": "${workspaceFolder}/robogauge/scripts/run.py", "args": [ - "--task", "go2_moe_flat", + "--task", "go2_moe.flat", "--experiment-name", "debug", "--seed", "0", "--friction", "1.6", @@ -24,13 +24,13 @@ "request": "launch", "program": "${workspaceFolder}/robogauge/scripts/run.py", "args": [ - "--task", "go2_moe_slope", + "--task", "go2_moe.slope", "--experiment-name", "debug", "--seed", "0", - "--search-max-level", - "--seeds", "0", "1", "2", + // "--search-max-level", + // "--seeds", "0", "1", "2", "--frictions", "1", - "--headless" + // "--headless" ], "console": "integratedTerminal" }, diff --git a/CMD.md b/CMD.md index 977ece8..8e30749 100644 --- a/CMD.md +++ b/CMD.md @@ -1,7 +1,7 @@ # Single Pipeline ```bash python robogauge/scripts/run.py \ - --task go2_moe_flat \ + --task go2_moe.flat \ --experiment-name debug \ --headless ``` @@ -9,28 +9,30 @@ python robogauge/scripts/run.py \ # Multi Pipeline ```bash python robogauge/scripts/run.py \ - --task go2_moe_flat \ + --task go2_moe.flat \ --experiment-name debug \ --multi \ - --num-processes 3 \ - --headless - -python robogauge/scripts/run.py \ - --task go2_moe_flat \ - --experiment-name debug \ - --multi \ - --num-processes 3 \ + --num-processes 5 \ + --seeds 0 1 2 3 4 \ + --frictions 0.5 1.0 1.5 2.0 2.5 \ --headless ``` # Level Pipeline ```bash +# Must specify single frictions for LevelPipeline! python robogauge/scripts/run.py \ - --task go2_slope \ + --task go2_moe.slope \ --experiment-name debug \ - --seed 0 \ - --headless \ - --search-max-level --seeds 0 1 2 --frictions 1 + --search-max-level \ + --seeds 0 1 2 \ + --frictions 1.0 \ + --headless +``` + +# Stress Pipeline +```bash + ``` # Radar/Bar Plot diff --git a/UPDATE.md b/UPDATE.md index 6497b0e..e1873ad 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -1,4 +1,8 @@ # UPDATE +## 20251226 +### v0.1.16 +Bugs: level_results会存储到MultiPipeline下, 因为MultiPipeline不是子进程启动的, 会覆盖LevelPipeline的Logger冲突 +1. 基本完成StressPipeline, 继续调试完毕 ## 20251225 ### v0.1.15 1. 统一terrain的大小为10x10m, 某一边的中点在(0,0,0), 对全部的带等级的terrain都加上边界墙高度10m @@ -10,6 +14,7 @@ | mlp | 1 | 4 | 7 | 8 | 9 | 3. 发现有穿模问题, 加入穿模判定0.035m, BaesPipeline新增一个warning穿模报错, 发生穿模重置当前goal并重置环境, 不计入失败 + 3.1. 穿模判定在斜坡上会有很大的问题, 对slope一侧加上0.8m的可供站立平地, 并取消关于wall和floor的穿模判定 4. 发现可以通过同时增加台阶的宽度从而避免产生体积为0的接触点, 一定程度减少穿模问题 5. 判定Level通过比例为0.8, 也就是5个种子有4个通过即可 6. 完成stairs_down地形调试, 简单比较moe和mlp通过地形难度, 初始点和中点在y方向上有3m差距, 避免直接滚下去恰好通过 diff --git a/resources/terrains/slope/slope_1.xml b/resources/terrains/slope/slope_1.xml index c252ef2..966209e 100644 --- a/resources/terrains/slope/slope_1.xml +++ b/resources/terrains/slope/slope_1.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_10.xml b/resources/terrains/slope/slope_10.xml index 257c4f9..bbdc055 100644 --- a/resources/terrains/slope/slope_10.xml +++ b/resources/terrains/slope/slope_10.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_2.xml b/resources/terrains/slope/slope_2.xml index c829790..4e44345 100644 --- a/resources/terrains/slope/slope_2.xml +++ b/resources/terrains/slope/slope_2.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_3.xml b/resources/terrains/slope/slope_3.xml index e08ffee..8725216 100644 --- a/resources/terrains/slope/slope_3.xml +++ b/resources/terrains/slope/slope_3.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_4.xml b/resources/terrains/slope/slope_4.xml index 319d7af..d344938 100644 --- a/resources/terrains/slope/slope_4.xml +++ b/resources/terrains/slope/slope_4.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_5.xml b/resources/terrains/slope/slope_5.xml index b718bb0..cf814ba 100644 --- a/resources/terrains/slope/slope_5.xml +++ b/resources/terrains/slope/slope_5.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_6.xml b/resources/terrains/slope/slope_6.xml index dc98973..a21c05e 100644 --- a/resources/terrains/slope/slope_6.xml +++ b/resources/terrains/slope/slope_6.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_7.xml b/resources/terrains/slope/slope_7.xml index 0dd2c95..8b37f3a 100644 --- a/resources/terrains/slope/slope_7.xml +++ b/resources/terrains/slope/slope_7.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_8.xml b/resources/terrains/slope/slope_8.xml index b15c594..2f08d60 100644 --- a/resources/terrains/slope/slope_8.xml +++ b/resources/terrains/slope/slope_8.xml @@ -14,6 +14,6 @@ - + diff --git a/resources/terrains/slope/slope_9.xml b/resources/terrains/slope/slope_9.xml index d5552cf..0d6ba69 100644 --- a/resources/terrains/slope/slope_9.xml +++ b/resources/terrains/slope/slope_9.xml @@ -14,6 +14,6 @@ - + diff --git a/robogauge/scripts/run_eval_models.sh b/robogauge/scripts/run_eval_models.sh index 5b98632..09c059d 100755 --- a/robogauge/scripts/run_eval_models.sh +++ b/robogauge/scripts/run_eval_models.sh @@ -4,8 +4,8 @@ # specified in default ./evaluate_models.txt file. # # Usage: ./run_scripts.sh [-n EXP_NAME] [-t TASK_NAME] [-s] -# -n EXP_NAME Experiment name (default: go2_moe_flat) -# -t TASK_NAME Task name (default: go2_moe_flat) +# -n EXP_NAME Experiment name (default: go2_moe.flat) +# -t TASK_NAME Task name (default: go2_moe.flat) # -s Save video (default: false) # -h Show this help message @@ -16,7 +16,7 @@ RUN_PY="$SCRIPT_DIR/run.py" ### Default Configure ### EXP_NAME="" # Experiment name [-n] -TASK_NAME="go2_moe_flat" # Task name [-t] +TASK_NAME="go2_moe.flat" # Task name [-t] SAVE_VIDEO=false # Whether to save video [-s] ### Parse Arguments ### diff --git a/robogauge/tasks/__init__.py b/robogauge/tasks/__init__.py index 2dbfb3c..5a2c480 100644 --- a/robogauge/tasks/__init__.py +++ b/robogauge/tasks/__init__.py @@ -1,19 +1,24 @@ from robogauge.utils.task_register import task_register from robogauge.tasks.simulator.mujoco_config import MujocoConfig -from robogauge.tasks.robots import RobotConfig, Go2Config, Go2MoEConfig +from robogauge.tasks.robots import RobotConfig, Go2Config, Go2MoEConfig, Go2TerrainConfig, Go2MoETerrainConfig from robogauge.tasks.pipeline import BasePipeline from robogauge.tasks.gauge import BaseGaugeConfig from robogauge.tasks.custom.go2 import * +# Register tasks: Task name format '.' task_register.register('base', BasePipeline, MujocoConfig, BaseGaugeConfig, RobotConfig) -task_register.register('go2_flat', BasePipeline, MujocoConfig, Go2FlatGaugeConfig, Go2Config) -task_register.register('go2_moe_flat', BasePipeline, MujocoConfig, Go2FlatGaugeConfig, Go2MoEConfig) -task_register.register('go2_slope', BasePipeline, Go2SlopeMujocoConfig, Go2SlopeGaugeConfig, Go2Config) -task_register.register('go2_moe_slope', BasePipeline, Go2SlopeMujocoConfig, Go2SlopeGaugeConfig, Go2MoEConfig) -task_register.register('go2_wave', BasePipeline, MujocoConfig, Go2WaveGaugeConfig, Go2Config) -task_register.register('go2_moe_wave', BasePipeline, MujocoConfig, Go2WaveGaugeConfig, Go2MoEConfig) -task_register.register('go2_stairs_up', BasePipeline, MujocoConfig, Go2StairsUpGaugeConfig, Go2Config) -task_register.register('go2_moe_stairs_up', BasePipeline, MujocoConfig, Go2StairsUpGaugeConfig, Go2MoEConfig) -task_register.register('go2_stairs_down', BasePipeline, MujocoConfig, Go2StairsDownGaugeConfig, Go2Config) -task_register.register('go2_moe_stairs_down', BasePipeline, MujocoConfig, Go2StairsDownGaugeConfig, Go2MoEConfig) \ No newline at end of file + +# Go2 MLP +task_register.register('go2.flat', BasePipeline, MujocoConfig, Go2FlatGaugeConfig, Go2Config) +task_register.register('go2.slope', BasePipeline, MujocoConfig, Go2SlopeGaugeConfig, Go2TerrainConfig) +task_register.register('go2.wave', BasePipeline, MujocoConfig, Go2WaveGaugeConfig, Go2TerrainConfig) +task_register.register('go2.stairs_up', BasePipeline, MujocoConfig, Go2StairsUpGaugeConfig, Go2TerrainConfig) +task_register.register('go2.stairs_down', BasePipeline, MujocoConfig, Go2StairsDownGaugeConfig, Go2TerrainConfig) + +# Go2 MoE +task_register.register('go2_moe.flat', BasePipeline, MujocoConfig, Go2FlatGaugeConfig, Go2MoEConfig) +task_register.register('go2_moe.slope', BasePipeline, MujocoConfig, Go2SlopeGaugeConfig, Go2MoETerrainConfig) +task_register.register('go2_moe.wave', BasePipeline, MujocoConfig, Go2WaveGaugeConfig, Go2MoETerrainConfig) +task_register.register('go2_moe.stairs_up', BasePipeline, MujocoConfig, Go2StairsUpGaugeConfig, Go2MoETerrainConfig) +task_register.register('go2_moe.stairs_down', BasePipeline, MujocoConfig, Go2StairsDownGaugeConfig, Go2MoETerrainConfig) diff --git a/robogauge/tasks/custom/go2/__init__.py b/robogauge/tasks/custom/go2/__init__.py index 86b064b..b1b0b6b 100644 --- a/robogauge/tasks/custom/go2/__init__.py +++ b/robogauge/tasks/custom/go2/__init__.py @@ -7,7 +7,7 @@ @Blog : https://wty-yy.github.io/ ''' from .go2_flat_task import Go2FlatGaugeConfig -from .go2_slope_task import Go2SlopeGaugeConfig, Go2SlopeMujocoConfig +from .go2_slope_task import Go2SlopeGaugeConfig from .go2_wave_task import Go2WaveGaugeConfig from .go2_stairs_up_task import Go2StairsUpGaugeConfig from .go2_stairs_down_task import Go2StairsDownGaugeConfig diff --git a/robogauge/tasks/custom/go2/go2_slope_task.py b/robogauge/tasks/custom/go2/go2_slope_task.py index 8dff561..55bb7cf 100644 --- a/robogauge/tasks/custom/go2/go2_slope_task.py +++ b/robogauge/tasks/custom/go2/go2_slope_task.py @@ -17,8 +17,3 @@ class Go2SlopeGaugeConfig(SlopeGaugeConfig): enabled = True soft_dof_limit_ratio = 0.7 dof_names = ['hip', 'thigh'] # List of DOF names to monitor, None for all - -class Go2SlopeMujocoConfig(MujocoConfig): - class domain_rand(MujocoConfig.domain_rand): - action_delay = True - friction = 2.4 diff --git a/robogauge/tasks/gauge/base_gauge_config.py b/robogauge/tasks/gauge/base_gauge_config.py index 3f2e14b..b60fcbc 100644 --- a/robogauge/tasks/gauge/base_gauge_config.py +++ b/robogauge/tasks/gauge/base_gauge_config.py @@ -22,7 +22,8 @@ class BaseGaugeConfig(Config): class goals: class max_velocity: # goal with maximum velocity enabled = False - cmd_duration = 5.0 # [s] duration for each velocity command + move_duration = 5.0 # [s] duration for each velocity command + stance_duration = 2.0 # [s] duration for each stance (no movement) class diagonal_velocity: # goal with diagonal velocity changes enabled = False diff --git a/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py index 280e214..1478f70 100644 --- a/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py +++ b/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py @@ -14,19 +14,20 @@ class SlopeGaugeConfig(BaseGaugeConfig): class assets(BaseGaugeConfig.assets): terrain_name = "slope" - terrain_level = 10 # 1-10 + terrain_level = 4 # 1-10 terrain_xmls = [ - '{ROBOGAUGE_ROOT_DIR}/resources/terrains/slope/slope_10.xml', + '{ROBOGAUGE_ROOT_DIR}/resources/terrains/slope/slope_4.xml', '{ROBOGAUGE_ROOT_DIR}/resources/terrains/wall/10x10_wall.xml', ] - terrain_spawn_pos = [0.8, 0, 1] # x y z [m], robot freejoint spawn position on the terrain + terrain_spawn_pos = [0.5, 0, 0] # x y z [m], robot freejoint spawn position on the terrain class goals(BaseGaugeConfig.goals): class target_pos_velocity: # goal to reach a target position by velocity command enabled = True - target_pos = [4, 0, 2.28] # x y z [m], target position in the environment, used for target position goal + target_pos = [4.8, 0, 1.152 + 0.1] # x y z [m], target position in the environment, used for target position goal lin_vel_x = 1.0 # +/- m/s lin_vel_y = 1.0 # +/- m/s ang_vel_yaw = 1.5 # +/- rad/s max_cmd_duration = 20.0 # [s] maximum duration to reach the target position reach_threshold = 0.1 + \ No newline at end of file diff --git a/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py index 1c5b0c0..ff5fa1a 100644 --- a/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py +++ b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py @@ -9,7 +9,7 @@ ''' from robogauge.utils.config import Config -class TerrainLevelsConfig(Config): +class TerrainSearchLevelsConfig(Config): class flat: levels = [0] targets = [[4, 0, 0]] # target positions for each level, if target goal is used @@ -17,28 +17,28 @@ class TerrainLevelsConfig(Config): class slope: levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] targets = [ - [4, 0, 0.588 + 0.1], - [4, 0, 0.776 + 0.1], - [4, 0, 0.964 + 0.1], - [4, 0, 1.152 + 0.1], - [4, 0, 1.340 + 0.1], - [4, 0, 1.528 + 0.1], - [4, 0, 1.716 + 0.1], - [4, 0, 1.904 + 0.1], - [4, 0, 2.092 + 0.1], - [4, 0, 2.280 + 0.1], + [4.8, 0, 0.588 + 0.1], + [4.8, 0, 0.776 + 0.1], + [4.8, 0, 0.964 + 0.1], + [4.8, 0, 1.152 + 0.1], + [4.8, 0, 1.340 + 0.1], + [4.8, 0, 1.528 + 0.1], + [4.8, 0, 1.716 + 0.1], + [4.8, 0, 1.904 + 0.1], + [4.8, 0, 2.092 + 0.1], + [4.8, 0, 2.280 + 0.1], ] spawns = [ - [0.8, 0, 0.55 - 0.037 * 9], - [0.8, 0, 0.55 - 0.037 * 8], - [0.8, 0, 0.55 - 0.037 * 7], - [0.8, 0, 0.55 - 0.037 * 6], - [0.8, 0, 0.55 - 0.037 * 5], - [0.8, 0, 0.55 - 0.037 * 4], - [0.8, 0, 0.55 - 0.037 * 3], - [0.8, 0, 0.55 - 0.037 * 2], - [0.8, 0, 0.55 - 0.037], - [0.8, 0, 0.55], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], + [0.5, 0, 0], ] class wave: @@ -98,3 +98,19 @@ class TerrainLevelsConfig(Config): [1.1, 3.0, 7 - 0.3], [1.1, 3.0, 7], ] + +class TerrainEvalLevelsConfig(Config): + class slope: + levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + spawns = [ + [5.0, 0, 4.2 * (0.57 - 0.047 * 9)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 8)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 7)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 6)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 5)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 4)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 3)], + [5.0, 0, 4.2 * (0.57 - 0.047 * 2)], + [5.0, 0, 4.2 * (0.57 - 0.047)], + [5.0, 0, 4.2 * 0.57], + ] diff --git a/robogauge/tasks/pipeline/__init__.py b/robogauge/tasks/pipeline/__init__.py index 9b1a2a0..8d0f0c9 100644 --- a/robogauge/tasks/pipeline/__init__.py +++ b/robogauge/tasks/pipeline/__init__.py @@ -1 +1,4 @@ -from .base_pipeline import BasePipeline \ No newline at end of file +from .base_pipeline import BasePipeline +from .multi_pipeline import MultiPipeline +from .level_pipeline import LevelPipeline +from .stress_pipeline import StressPipeline diff --git a/robogauge/tasks/pipeline/level_pipeline.py b/robogauge/tasks/pipeline/level_pipeline.py index 038c944..73f80a1 100644 --- a/robogauge/tasks/pipeline/level_pipeline.py +++ b/robogauge/tasks/pipeline/level_pipeline.py @@ -10,17 +10,19 @@ import yaml from robogauge.tasks.pipeline.multi_pipeline import MultiPipeline -from robogauge.utils.logger import logger +from robogauge.utils.logger import Logger + +level_logger = Logger() # LevelPipeline logger class LevelPipeline: def __init__(self, args): self.args = args self.seeds = args.seeds - logger.create(args.experiment_name+'_level', args.run_name) + level_logger.create(args.experiment_name+'_level', args.run_name) def run(self): - logger.info(f"🚀 Starting Level Seacher for '{self.args.experiment_name}'.") - logger.info(f"🔢 Seeds: {self.seeds}") + level_logger.info(f"🚀 Starting Level Searcher for '{self.args.experiment_name}'.") + level_logger.info(f"🔢 Seeds: {self.seeds}") # binary search levels l, r = 0, 10 @@ -40,22 +42,22 @@ class LevelPipeline: 'terrain_level': 0, }) if level >= 1: - logger.info(f"🏆 Found maximum level: {level}") + level_logger.info(f"🏆 Found maximum level: {level}") else: - logger.info(f"❌ No valid level found [1-10].") - with open(logger.log_dir / "level_search_results.yaml", 'w') as f: + level_logger.info(f"❌ No valid level found [1-10].") + with open(level_logger.log_dir / "level_search_results.yaml", 'w') as f: yaml.dump(level_results, f, allow_unicode=True, sort_keys=False) return level, level_results - def test_level(self, level: int) -> bool: - logger.info(f"🔍 Testing level {level}...") + def test_level(self, level: int): + level_logger.info(f"🔍 Testing level {level}...") self.args.level = level multi_pipeline = MultiPipeline(self.args) aggregated_results = multi_pipeline.run() success_mean = float(aggregated_results['success']['mean'].split(' ')[0]) all_success = success_mean >= 0.8 if all_success: - logger.info(f"✅ Level {level} passed all tests.") + level_logger.info(f"✅ Level {level} passed all tests.") else: - logger.info(f"❌ Level {level} failed some tests.") + level_logger.info(f"❌ Level {level} failed some tests.") return all_success, aggregated_results diff --git a/robogauge/tasks/pipeline/multi_pipeline.py b/robogauge/tasks/pipeline/multi_pipeline.py index b81ac3e..cdad959 100644 --- a/robogauge/tasks/pipeline/multi_pipeline.py +++ b/robogauge/tasks/pipeline/multi_pipeline.py @@ -21,9 +21,12 @@ from collections import defaultdict from robogauge.tasks.pipeline.base_pipeline import BasePipeline from robogauge.utils.task_register import task_register -from robogauge.utils.logger import logger +from robogauge.utils.logger import Logger + +multi_logger = Logger() # MultiPipeline logger def run_single_process(args, data): + from robogauge.utils.logger import logger seed, base_mass, friction = data local_args = deepcopy(args) local_args.seed = seed @@ -41,6 +44,7 @@ def run_single_process(args, data): ret = { 'status': 'success', 'results': results, + 'data': data, 'model_path': pipeline.robot_cfg.control.model_path, } if warning is not None: @@ -65,7 +69,7 @@ class MultiPipeline: self.base_masses = args.base_masses self.num_processes = args.num_processes self.static_info = {} - logger.create(args.experiment_name+'_multi', args.run_name+'_multi') + multi_logger.create(args.experiment_name+'_multi', args.run_name+'_multi') def add_static_info(self, key: str, value): if key not in self.static_info: @@ -74,55 +78,54 @@ class MultiPipeline: assert self.static_info[key] == value, f"Static info key '{key}' has conflicting values: {self.static_info[key]} vs {value}" def run(self): - logger.info(f"🚀 Starting Multi-Process Evaluation with {self.num_processes} processes.") - logger.info(f"🔢 Seeds: {self.seeds}, Frictions: {self.frictions}, Base masses: {self.base_masses}") + multi_logger.info(f"🚀 Starting Multi-Process Evaluation with {self.num_processes} processes.") + 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)) ctx = multiprocessing.get_context('spawn') worker_func = functools.partial(run_single_process, self.args) results_list = [] - success_flags = [] with ctx.Pool(processes=self.num_processes) as pool: iterator = pool.imap_unordered(worker_func, workers_data) for results in tqdm(iterator, total=len(workers_data), desc="Evaluation"): - success_flags.append(results['status'] == 'success') - - results_list.append(results['results']) + results_list.append(results) self.add_static_info('model_path', results['model_path']) self.add_static_info('terrain_name', results['results']['terrain_name']) self.add_static_info('terrain_level', results['results']['terrain_level']) if results['status'] != 'success': data = results['data'] - 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']}") - logger.info("✅ Multi-Process Evaluation Completed.") - aggregated_results = self.aggregate_results(results_list, success_flags, workers_data) + multi_logger.info("✅ Multi-Process Evaluation Completed.") + aggregated_results = self.aggregate_results(results_list) return aggregated_results - def aggregate_results(self, all_results, success_flags, workers_data): + def aggregate_results(self, all_results): """ Process results from all processes and aggregate them. """ - logger.info("📊 Aggregating Results from all runs...") + multi_logger.info("📊 Aggregating Results from all runs...") summary = {'success': {}, **self.static_info} finish_msg = ( f"""\n{'='*20} Run Finish Summary {'='*20}\n""" f"""{'Seed':^10}{'Base Mass':^15}{'Friction':^15}{'Status':^10}\n""" ) - for success, data in zip(success_flags, workers_data): - seed, base_mass, friction = data + all_results = sorted(all_results, key=lambda x: x['data']) + for result in all_results: + seed, base_mass, friction = result['data'] + success = result['status'] == 'success' status_str = "✅" if success else "❌" finish_msg += f"{seed:^10}{base_mass:^15}{friction:^15}{status_str:^10}\n" summary['success'][f"Seed_{seed}_BaseMass_{base_mass}_Friction_{friction}"] = True if success else False finish_msg += f"""{'='*88}""" - logger.info(finish_msg) + multi_logger.info(finish_msg) if not all_results: - logger.error("No results to aggregate.") + multi_logger.error("No results to aggregate.") return value_collections = defaultdict(lambda: defaultdict(list)) for result in all_results: - for goal, metrics in result.items(): + for goal, metrics in result['results'].items(): if goal != 'summary': continue for metric, means in metrics.items(): @@ -134,13 +137,13 @@ class MultiPipeline: for mean_name, values in means.items(): summary[metric][mean_name] = f"{float(np.mean(values)):.4f} ± {float(np.std(values)):.4f}" - save_path = logger.log_dir / "aggregated_results.yaml" + save_path = multi_logger.log_dir / "aggregated_results.yaml" with open(save_path, 'w') as file: yaml.dump(summary, file, allow_unicode=True, sort_keys=False) - logger.info("✅ Aggregated execution finished.") - logger.info(f"📁 Aggregated results saved to: {save_path}") + multi_logger.info("✅ Aggregated execution finished.") + multi_logger.info(f"📁 Aggregated results saved to: {save_path}") - logger.info( + multi_logger.info( f"""\n{'='*20} Multi-Run Summary {'='*20}\n""" f"""{yaml.dump(summary, allow_unicode=True)}""" f"""{'='*60}""" diff --git a/robogauge/tasks/pipeline/stress_pipeline.py b/robogauge/tasks/pipeline/stress_pipeline.py new file mode 100644 index 0000000..8660c3c --- /dev/null +++ b/robogauge/tasks/pipeline/stress_pipeline.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +''' +@File : stress_pipeline.py +@Time : 2025/12/25 21:20:43 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Stress Pipeline for Robogauge +''' +import yaml +import functools +from tqdm import tqdm +import multiprocessing +from copy import deepcopy +from itertools import product + +from robogauge.utils.logger import Logger +from robogauge.tasks.pipeline import MultiPipeline, LevelPipeline +from robogauge.tasks.gauge.gauge_configs.terrain_levels_config import TerrainSearchLevelsConfig + +stress_logger = Logger() # StressPipeline logger + +def run_pipeline(args, data): + args = deepcopy(args) + search = data['search_max_level'] + if search is True: + args.friction = data['friction'] + args.frictions = [data['friction']] + args.base_mass = data['base_mass'] + args.base_masses = [data['base_mass']] + args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}" + args.experiment_name = f"{args.experiment_name}_{data['terrain_name']}_baseMass{data['base_mass']}_friction{data['friction']}" + else: + args.task_name = f"{data['task_robot_model']}.{data['terrain_name']}" + args.experiment_name = f"{args.experiment_name}_{data['terrain_name']}" + + level = None # flat terrain + if search: + level, results = LevelPipeline(args).run() + + if level == 0: # no valid level found + results = { + 'success': False, + 'results': results, + 'data': data, + } + else: + args.level = level + results = { + 'success': True, + 'results': MultiPipeline(args).run(), + 'data': data, + } + + return results + +class StressPipeline: + def __init__(self, args): + self.args = args + self.seeds = args.seeds + self.task_robot_model = args.task_name.split('.')[0] + self.num_processes = args.stress_num_processes + args.experiment_name = self.task_robot_model + '_stress' + ('' if args.cli_experiment_name is None else '_' + args.cli_experiment_name) + self.static_info = {} + stress_logger.create(args.experiment_name, args.run_name) + + def add_static_info(self, key: str, value): + if key not in self.static_info: + self.static_info[key] = value + else: + assert self.static_info[key] == value, f"Static info key '{key}' has conflicting values: {self.static_info[key]} vs {value}" + + def run(self): + stress_logger.info(f"🚀 Starting Stress Benchmark for '{self.args.experiment_name}'.") + stress_logger.info(f"🔢 Seeds: {self.seeds}") + terrain_names = self.args.stress_terrain_names + stress_logger.info(f"🌄 Stress Test Terrain Names: {terrain_names}") + + ctx = multiprocessing.get_context('spawn') + worker_func = functools.partial(run_pipeline, self.args) + + ### Build worker data ### + workers_data = [] + terrain_search_levels_config = TerrainSearchLevelsConfig() + for terrain_name in terrain_names: + search_max_level = True + terrain_level_cfg = getattr(terrain_search_levels_config, terrain_name, None) + assert terrain_level_cfg is not None, f"Terrain '{terrain_name}' not found in TerrainSearchLevelsConfig." + if len(terrain_level_cfg.levels) == 1: # Flattened terrain + search_max_level = False + data = { + 'task_robot_model': self.task_robot_model, + 'terrain_name': terrain_name, + 'search_max_level': search_max_level, + } + if search_max_level: + for friction, base_mass in product(self.args.frictions, self.args.base_masses): + data.update({ + 'friction': friction, + 'base_mass': base_mass, + }) + workers_data.append(data) + + ### Run and collect results ### + results_list = [] + with ctx.Pool(processes=self.num_processes) as pool: + iterator = pool.imap_unordered(worker_func, workers_data) + for results in tqdm(iterator, total=len(workers_data), desc="Stress Benchmark"): + results_list.append(results) + self.add_static_info('model_path', results['results']['model_path']) + + stress_logger.info("✅ Stress Benchmark Completed.") + stress_results = self.aggregate_results(results_list) + return stress_results + + def aggregate_results(self, all_results): + stress_logger.info("📊 Aggregating Stress Benchmark Results...") + summary = {} + finish_msg = ( + f"""\n{'='*20} Stress Benchmark Summary {'='*20}\n""" + f"""{'Terrain Name':^20}{'Base Mass':^15}{'Friction':^15}{'Status':^10}\n""" + ) + all_results = sorted(all_results, key=lambda x: (x['data']['terrain_name'], x['data'].get('base_mass', 0), x['data'].get('friction', 0))) + for result in all_results: + terrain_name = result['data']['terrain_name'] + base_mass = result['data'].get('base_mass', self.args.base_masses) + friction = result['data'].get('friction', self.args.frictions) + status = "✅" if result['success'] else "❌" + finish_msg += f"{terrain_name:^20}{str(base_mass):^15}{str(friction):^15}{status:^10}\n" + finish_msg += f"""{'='*66}""" + stress_logger.info(finish_msg) diff --git a/robogauge/tasks/robots/__init__.py b/robogauge/tasks/robots/__init__.py index f9bc6a5..c8f7d39 100644 --- a/robogauge/tasks/robots/__init__.py +++ b/robogauge/tasks/robots/__init__.py @@ -1,6 +1,6 @@ from .base_robot_config import RobotConfig from .base_robot import BaseRobot -from .go2.go2_config import Go2Config +from .go2.go2_config import Go2Config, Go2TerrainConfig from .go2.go2 import Go2 -from .go2.go2_moe_config import Go2MoEConfig +from .go2.go2_moe_config import Go2MoEConfig, Go2MoETerrainConfig from .go2.go2_moe import Go2MoE diff --git a/robogauge/tasks/robots/go2/go2_config.py b/robogauge/tasks/robots/go2/go2_config.py index 48ccfa5..068a2e0 100644 --- a/robogauge/tasks/robots/go2/go2_config.py +++ b/robogauge/tasks/robots/go2/go2_config.py @@ -48,9 +48,15 @@ class Go2Config(RobotConfig): class commands(RobotConfig.commands): lin_vel_x = [-2.0, 2.0] # min max [m/s] - lin_vel_y = [-2.0, 2.0] # min max [m/s] + lin_vel_y = [-1.0, 1.0] # min max [m/s] lin_vel_z = None # min max [m/s] ang_vel_roll = None # min max [rad/s] ang_vel_pitch = None # min max [rad/s] ang_vel_yaw = [-2, 2] # min max [rad/s] +class Go2TerrainConfig(Go2Config): + """ Go2 Robot Configuration for Terrain Tasks (wave, stairs up/down, slope, obstacles) """ + class commands(Go2Config.commands): + lin_vel_x = [-1.0, 1.0] # min max [m/s] + lin_vel_y = [-1.0, 1.0] # min max [m/s] + ang_vel_yaw = [-1.5, 1.5] # min max [rad/s] diff --git a/robogauge/tasks/robots/go2/go2_moe_config.py b/robogauge/tasks/robots/go2/go2_moe_config.py index 5799028..3de123c 100644 --- a/robogauge/tasks/robots/go2/go2_moe_config.py +++ b/robogauge/tasks/robots/go2/go2_moe_config.py @@ -14,3 +14,10 @@ class Go2MoEConfig(Go2Config): class control(Go2Config.control): model_path = "{ROBOGAUGE_ROOT_DIR}/resources/models/go2/go2_moe_cts_124k.pt" + +class Go2MoETerrainConfig(Go2MoEConfig): + """ Go2 MoE Robot Configuration for Terrain Tasks (wave, stairs up/down, slope, obstacles) """ + class commands(Go2MoEConfig.commands): + lin_vel_x = [-1.0, 1.0] # min max [m/s] + lin_vel_y = [-1.0, 1.0] # min max [m/s] + ang_vel_yaw = [-1.5, 1.5] # min max [rad/s] diff --git a/robogauge/tasks/simulator/mujoco_config.py b/robogauge/tasks/simulator/mujoco_config.py index 1df2a1b..170521e 100644 --- a/robogauge/tasks/simulator/mujoco_config.py +++ b/robogauge/tasks/simulator/mujoco_config.py @@ -49,4 +49,6 @@ class MujocoConfig(Config): class truncation: enabled = True projected_gravity_rad = 2.5 # [rad], if gravity projection angle exceeds this value, truncate episode - penetration_threshold = -0.05 # [m], if any contact penetration depth is below this threshold, truncate episode + penetration_threshold = -0.035 # [m], if any contact penetration depth is below this threshold, truncate episode + skip_penetration_geoms = ['wall', 'floor'] # Geometries to skip penetration check + skip_self_penetration = True # Whether to check self-penetration diff --git a/robogauge/tasks/simulator/mujoco_simulator.py b/robogauge/tasks/simulator/mujoco_simulator.py index 2cb6b48..d0eee6e 100644 --- a/robogauge/tasks/simulator/mujoco_simulator.py +++ b/robogauge/tasks/simulator/mujoco_simulator.py @@ -283,9 +283,19 @@ class MujocoSimulator: if -projected_gravity[2] < np.cos(self.cfg.truncation.projected_gravity_rad): raise RuntimeError(f"[Roll Error] Episode truncated due to excessive projected gravity, angle: {np.arccos(-projected_gravity[2]):.3f} rad, projected: {projected_gravity}") - # is_penetrated, geom1, geom2, dist = self.check_penetration(self.cfg.truncation.penetration_threshold) - # if is_penetrated: - # raise RuntimeError(f"[Penetration Error] Episode truncated: Penetration ({geom1} <-> {geom2}), distance: {dist}") + is_penetrated, geom1, geom2, dist = self.check_penetration(self.cfg.truncation.penetration_threshold) + if is_penetrated: + flat = True + if self.cfg.truncation.skip_penetration_geoms is not None and ( + any(skip_geom in geom1.lower() for skip_geom in self.cfg.truncation.skip_penetration_geoms) or + any(skip_geom in geom2.lower() for skip_geom in self.cfg.truncation.skip_penetration_geoms) + ): + flat = False + if self.cfg.truncation.skip_self_penetration: + if geom1.split('/')[0] == geom2.split('/')[0]: + flat = False + if flat: + raise RuntimeError(f"[Penetration Error] Episode truncated: Penetration ({geom1} <-> {geom2}), distance: {dist}") def reset(self): """ Reset the simulator to initial state. """ diff --git a/robogauge/utils/helpers.py b/robogauge/utils/helpers.py index fa978f0..93fa544 100644 --- a/robogauge/utils/helpers.py +++ b/robogauge/utils/helpers.py @@ -80,20 +80,27 @@ def parse_args(): # Multiprocessing parameters, with different seeds {"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], "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": "--frictions", "type": float, "nargs": "+", "default": [0.4, 0.7, 1.0, 1.3, 1.6], "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."}, # Level pipeline parameters {"name": "--search-max-level", "action": "store_true", "default": False, "help": "Use level pipeline to search maximum level."}, + + # Stress pipeline parameters + {"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-num-processes", "type": int, "default": 2, "help": "Number of parallel processes for stress benchmark."}, ] for param in parameters: parser.add_argument(param['name'], **{k: v for k, v in param.items() if k != 'name'}) args = parser.parse_args() + flatten_task_name = args.task_name.replace('.', '_') + args.cli_experiment_name = args.experiment_name if args.experiment_name is not None: - args.experiment_name = f"{args.task_name}_{args.experiment_name}" + args.experiment_name = f"{flatten_task_name}_{args.experiment_name}" else: - args.experiment_name = args.task_name + args.experiment_name = flatten_task_name return args def snake_to_pascal(s: str) -> str: diff --git a/robogauge/utils/task_register.py b/robogauge/utils/task_register.py index ad0ff34..f898dd4 100644 --- a/robogauge/utils/task_register.py +++ b/robogauge/utils/task_register.py @@ -10,7 +10,7 @@ from robogauge import ROBOGAUGE_ROOT_DIR from robogauge.utils.logger import logger from robogauge.utils.helpers import parse_args, set_seed -from robogauge.tasks.gauge.gauge_configs.terrain_levels_config import TerrainLevelsConfig +from robogauge.tasks.gauge.gauge_configs.terrain_levels_config import TerrainSearchLevelsConfig class TaskRegister(): def __init__(self): @@ -72,7 +72,7 @@ class TaskRegister(): sim_cfg.domain_rand.base_mass = args.base_mass if args.level is not None: gauger_cfg.assets.terrain_level = args.level - levels_cfg = TerrainLevelsConfig() + levels_cfg = TerrainSearchLevelsConfig() cfg = getattr(levels_cfg, gauger_cfg.assets.terrain_name) assert cfg is not None, f"Level {args.level} configuration not found in TerrainLevelsConfig." assert args.level in cfg.levels, f"Level must be in {cfg.levels}."