diff --git a/UPDATE.md b/UPDATE.md
index cfc8b01..58763d1 100644
--- a/UPDATE.md
+++ b/UPDATE.md
@@ -1,7 +1,11 @@
# UPDATE
+TODO: 处理LevelPipeline返回的result信息
## 20251222
### v0.1.13
1. 即使模型崩溃也要继续测完后续的goals, 但是跳过当前的sub goals
+2. 加入当翻滚角>2.5rad时认为崩溃, 自动切换为下一个goal
+3. 初步完成二分查找level等级, 细节如下
+ LevelPipeline: 一种二分查找当前域随机化(friction, base_mass)下的最大成功terrain等级, 每次测试中采用三个seeds, 调用MultiPipeline同时测试三个seeds, 如果同时全部成功, 则判定为通过
## 20251221
### v0.1.12
1. 在模型崩溃时也记录下最后的gauge信息, 修改single/multi pipeline逻辑
diff --git a/resources/terrains/slope/slope_1.xml b/resources/terrains/slope/slope_1.xml
index f8c90ac..7b86709 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 b61559d..dff0a69 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 8e8b7a0..d29e1b7 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 eae16cd..fa64ae6 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 ad3fd52..3803000 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 180f4cf..6668868 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 d53ac48..93dc2b3 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 f3bdf46..330de1c 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 c9d9170..6ec33c1 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 e23df0f..57c7e8f 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.py b/robogauge/scripts/run.py
index b91d713..d992b6d 100644
--- a/robogauge/scripts/run.py
+++ b/robogauge/scripts/run.py
@@ -14,6 +14,7 @@ os.environ["MKL_NUM_THREADS"] = "1"
from robogauge.tasks import *
from robogauge.tasks.pipeline.multi_pipeline import MultiPipeline
+from robogauge.tasks.pipeline.level_pipeline import LevelPipeline
from robogauge.utils.task_register import task_register
from robogauge.utils.helpers import parse_args
@@ -22,9 +23,12 @@ from robogauge.utils.logger import logger
if __name__ == '__main__':
args = parse_args()
- if not args.multi:
- pipeline: BasePipeline = task_register.make_pipeline(args=args)
- pipeline.run()
- else:
+ if args.multi:
multi_pipeline = MultiPipeline(args)
multi_pipeline.run()
+ elif args.search_max_level:
+ level_pipeline = LevelPipeline(args)
+ level_pipeline.run()
+ else:
+ pipeline: BasePipeline = task_register.make_pipeline(args=args)
+ pipeline.run()
diff --git a/robogauge/tasks/gauge/base_gauge.py b/robogauge/tasks/gauge/base_gauge.py
index 344e390..69ae50e 100644
--- a/robogauge/tasks/gauge/base_gauge.py
+++ b/robogauge/tasks/gauge/base_gauge.py
@@ -152,7 +152,9 @@ class BaseGauge:
self.results['summary'][metric_name][quantile] = f"{mean:.4f} ± {std:.4f}"
save_path = Path(logger.log_dir) / "results.yaml"
- self.results["terrain"] = f"{self.cfg.assets.terrain_name}"
+ self.results["terrain_name"] = self.cfg.assets.terrain_name
+ self.results["terrain_level"] = self.cfg.assets.terrain_level
+
with open(save_path, 'w') as file:
yaml.dump(self.results, file, allow_unicode=True, sort_keys=False)
yaml_str = yaml.dump(self.results, allow_unicode=True, sort_keys=False)
diff --git a/robogauge/tasks/gauge/base_gauge_config.py b/robogauge/tasks/gauge/base_gauge_config.py
index 1ccffd1..f09676c 100644
--- a/robogauge/tasks/gauge/base_gauge_config.py
+++ b/robogauge/tasks/gauge/base_gauge_config.py
@@ -14,7 +14,8 @@ class BaseGaugeConfig(Config):
write_tensorboard = False # Whether to write tensorboard logs
class assets:
- terrain_name = "flat_0" # {type}_{level}
+ terrain_name = "flat"
+ terrain_level = 0
terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/flat.xml'
terrain_spawn_pos = [0, 0, 0] # x y z [m], robot freejoint spawn position on the terrain
diff --git a/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py
index e74f918..48ee998 100644
--- a/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py
+++ b/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py
@@ -13,6 +13,7 @@ class FlatGaugeConfig(BaseGaugeConfig):
gauge_class = 'BaseGauge'
class assets(BaseGaugeConfig.assets):
- terrain_name = "flat_0" # {type}_{level}
+ terrain_name = "flat"
+ terrain_level = 0
terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/flat.xml'
terrain_spawn_pos = [0, 0, 0] # x y z [m], robot freejoint spawn position on the terrain
diff --git a/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py
index 3a59d22..74b93a9 100644
--- a/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py
+++ b/robogauge/tasks/gauge/gauge_configs/slope_gauge_config.py
@@ -13,14 +13,15 @@ class SlopeGaugeConfig(BaseGaugeConfig):
gauge_class = 'BaseGauge'
class assets(BaseGaugeConfig.assets):
- terrain_name = "slope_5" # {type}_{level}
+ terrain_name = "slope"
+ terrain_level = 10 # 1-10
terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/slope/slope_10.xml'
terrain_spawn_pos = [0, 0, 0] # x y z [m], robot freejoint spawn position on the terrain
- class goals:
+ class goals(BaseGaugeConfig.goals):
class target_pos_velocity: # goal to reach a target position by velocity command
enabled = True
- target_pos = [4, 0, 2.0] # x y z [m], target position in the environment, used for target position goal
+ target_pos = [5, 0, 2.28] # 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
diff --git a/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py
new file mode 100644
index 0000000..10f7418
--- /dev/null
+++ b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py
@@ -0,0 +1,26 @@
+from robogauge.utils.config import Config
+
+class TerrainLevelsConfig(Config):
+ class flat:
+ levels = [0]
+ targets = [[4, 0, 0]] # target positions for each level, if target goal is used
+
+ class slope:
+ levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ targets = [
+ [5, 0, 0.588],
+ [5, 0, 0.776],
+ [5, 0, 0.964],
+ [5, 0, 1.152],
+ [5, 0, 1.340],
+ [5, 0, 1.528],
+ [5, 0, 1.716],
+ [5, 0, 1.904],
+ [5, 0, 2.092],
+ [5, 0, 2.280],
+ ]
+
+ class wave:
+ levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ targets = []
+
diff --git a/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py
index 9046361..461c474 100644
--- a/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py
+++ b/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py
@@ -13,7 +13,8 @@ class WaveGaugeConfig(BaseGaugeConfig):
gauge_class = 'BaseGauge'
class assets(BaseGaugeConfig.assets):
- terrain_name = "wave_1" # {type}_{level}
+ terrain_name = "wave"
+ terrain_level = 1 # 1-10
terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/wave/wave_1.xml'
terrain_spawn_pos = [1.5, 0, 1] # x y z [m], robot freejoint spawn position on the terrain
diff --git a/robogauge/tasks/pipeline/level_pipeline.py b/robogauge/tasks/pipeline/level_pipeline.py
new file mode 100644
index 0000000..c9c1b66
--- /dev/null
+++ b/robogauge/tasks/pipeline/level_pipeline.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+'''
+@File : level_pipeline.py
+@Time : 2025/12/22 20:38:11
+@Author : wty-yy
+@Version : 1.0
+@Blog : https://wty-yy.github.io/
+@Desc : Level Pipeline for Robogauge
+'''
+import yaml
+
+from robogauge.tasks.pipeline.multi_pipeline import MultiPipeline
+from robogauge.utils.logger import logger
+
+class LevelPipeline:
+ def __init__(self, args):
+ self.args = args
+ self.seeds = args.seeds
+ self.model_path = None
+ 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}")
+
+ # binary search levels
+ l, r = 0, 10
+ while l < r:
+ level = (l + r + 1) // 2
+ if self.test_level(level):
+ l = level
+ else:
+ r = level - 1
+ if l >= 1:
+ logger.info(f"🏆 Found maximum level: {l}")
+ else:
+ logger.info(f"❌ No valid level found [1-10].")
+
+ def test_level(self, level: int) -> bool:
+ logger.info(f"🔍 Testing level {level}...")
+ self.args.level = level
+ multi_pipeline = MultiPipeline(self.args)
+ log_dir = multi_pipeline.run()
+ # load results.yaml
+ with open(log_dir / "aggregated_results.yaml", 'r') as f:
+ results = yaml.safe_load(f)
+ self.model_path = results['model_path']
+ success_mean = float(results['success']['mean'].split(' ')[0])
+ all_success = success_mean == 1.0
+ if all_success:
+ logger.info(f"✅ Level {level} passed all tests.")
+ else:
+ logger.info(f"❌ Level {level} failed some tests.")
+ return all_success
diff --git a/robogauge/tasks/pipeline/multi_pipeline.py b/robogauge/tasks/pipeline/multi_pipeline.py
index dbce2de..b64d0ae 100644
--- a/robogauge/tasks/pipeline/multi_pipeline.py
+++ b/robogauge/tasks/pipeline/multi_pipeline.py
@@ -90,6 +90,7 @@ class MultiPipeline:
logger.info("✅ Multi-Process Evaluation Completed.")
self.aggregate_results(result_log_dirs, success_flags, workers_data)
+ return logger.log_dir
def aggregate_results(self, log_dirs, success_flags, workers_data):
""" Process results.yaml from each log_dir """
diff --git a/robogauge/tasks/simulator/mujoco_config.py b/robogauge/tasks/simulator/mujoco_config.py
index 0c94e4f..eee905e 100644
--- a/robogauge/tasks/simulator/mujoco_config.py
+++ b/robogauge/tasks/simulator/mujoco_config.py
@@ -45,3 +45,7 @@ class MujocoConfig(Config):
ang_vel = 0.8 # [rad/s]
joint_pos = 0.01 # [rad]
joint_vel = 3.0 # [rad/s]
+
+ class truncation:
+ enabled = True
+ projected_gravity_rad = 2.5 # [rad], if gravity projection angle exceeds this value, truncate episode
diff --git a/robogauge/tasks/simulator/mujoco_simulator.py b/robogauge/tasks/simulator/mujoco_simulator.py
index 1342f71..44f3443 100644
--- a/robogauge/tasks/simulator/mujoco_simulator.py
+++ b/robogauge/tasks/simulator/mujoco_simulator.py
@@ -20,6 +20,7 @@ from typing import Literal
from robogauge.utils.logger import logger
from robogauge.utils.helpers import parse_path
+from robogauge.utils.math_utils import get_projected_gravity
from robogauge.tasks.simulator.mujoco_config import MujocoConfig
from robogauge.tasks.simulator.sim_data import (
SimData,
@@ -261,8 +262,15 @@ class MujocoSimulator:
self.n_step += 1
self.sim_time = self.n_step * self.sim_dt
+ self.check_truncation(sim_data)
return sim_data
+ def check_truncation(self, sim_data: SimData):
+ if self.cfg.truncation.enabled:
+ projected_gravity = get_projected_gravity(sim_data.proprio.base.quat)
+ if -projected_gravity[2] < np.cos(self.cfg.truncation.projected_gravity_rad):
+ raise RuntimeError(f"Episode truncated due to excessive projected gravity, angle: {np.arccos(-projected_gravity[2]):.3f} rad, projected: {projected_gravity}")
+
def reset(self):
""" Reset the simulator to initial state. """
self.mj_physics.reset()
diff --git a/robogauge/utils/helpers.py b/robogauge/utils/helpers.py
index cf7e4fa..fa978f0 100644
--- a/robogauge/utils/helpers.py
+++ b/robogauge/utils/helpers.py
@@ -75,13 +75,17 @@ def parse_args():
{"name": "--plot-radar", "action": "store_true", "default": False, "help": "Plot radar charts for metrics."},
{"name": "--base-mass", "type": float, "default": 0.0, "help": "Set the base mass of the robot."},
{"name": "--friction", "type": float, "default": 1.0, "help": "Set the ground friction coefficient."},
+ {"name": "--level", "type": int, "help": "Set the difficulty level of the environment, range 1-10 (flat is 0)."},
# 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": "--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.4, 0.7, 1.0, 1.3, 1.6], "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."},
]
for param in parameters:
parser.add_argument(param['name'], **{k: v for k, v in param.items() if k != 'name'})
diff --git a/robogauge/utils/task_register.py b/robogauge/utils/task_register.py
index d7040e3..1d14976 100644
--- a/robogauge/utils/task_register.py
+++ b/robogauge/utils/task_register.py
@@ -10,6 +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
class TaskRegister():
def __init__(self):
@@ -69,5 +70,15 @@ class TaskRegister():
sim_cfg.domain_rand.friction = args.friction
if args.base_mass is not None:
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()
+ 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}."
+ gauger_cfg.goals.target_pos_velocity.target_pos = cfg.targets[cfg.levels.index(args.level)]
+ xml = gauger_cfg.assets.terrain_xml
+ xml = xml.rsplit('/', 1)[0] + f"/{gauger_cfg.assets.terrain_name}_{args.level}.xml"
+ gauger_cfg.assets.terrain_xml = xml
task_register = TaskRegister()