diff --git a/.vscode/launch.json b/.vscode/launch.json index abad633..a47b4fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,16 +5,34 @@ "version": "0.2.0", "configurations": [ { - "name": "Python 调试程序: run.py", + "name": "go2 moe flat single pipeline", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/robogauge/scripts/run.py", "args": [ "--task", "go2_moe_flat", "--experiment-name", "debug", + "--seed", "0", + "--friction", "1.6", "--headless" ], "console": "integratedTerminal" - } + }, + { + "name": "go2 moe slope level pipeline", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/robogauge/scripts/run.py", + "args": [ + "--task", "go2_moe_slope", + "--experiment-name", "debug", + "--seed", "0", + "--search-max-level", + "--seeds", "0", "1", "2", + "--frictions", "1", + "--headless" + ], + "console": "integratedTerminal" + }, ] } diff --git a/CMD.md b/CMD.md index e52fa8e..977ece8 100644 --- a/CMD.md +++ b/CMD.md @@ -1,4 +1,4 @@ -# Single Run +# Single Pipeline ```bash python robogauge/scripts/run.py \ --task go2_moe_flat \ @@ -6,7 +6,7 @@ python robogauge/scripts/run.py \ --headless ``` -# Multi Run +# Multi Pipeline ```bash python robogauge/scripts/run.py \ --task go2_moe_flat \ @@ -23,6 +23,16 @@ python robogauge/scripts/run.py \ --headless ``` +# Level Pipeline +```bash +python robogauge/scripts/run.py \ + --task go2_slope \ + --experiment-name debug \ + --seed 0 \ + --headless \ + --search-max-level --seeds 0 1 2 --frictions 1 +``` + # Radar/Bar Plot 将Multi Run结果绘制在雷达图中 ```bash diff --git a/UPDATE.md b/UPDATE.md index 58763d1..25cd7e0 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -1,5 +1,9 @@ # UPDATE -TODO: 处理LevelPipeline返回的result信息 +## 20251224 +### v0.1.14 +1. 处理LevelPipeline返回的result信息, 保存最高等级可通过地形返回的aggregated_results和level等级 +2. 完成wave地形的LevelPipeline支持 +3. 各种Pipeline的run直接返回dict信息, 不再是路径 ## 20251222 ### v0.1.13 1. 即使模型崩溃也要继续测完后续的goals, 但是跳过当前的sub goals diff --git a/resources/terrains/wave/wave.png b/resources/terrains/wave/wave.png new file mode 100644 index 0000000..80d6ddb Binary files /dev/null and b/resources/terrains/wave/wave.png differ diff --git a/resources/terrains/wave/wave_1.xml b/resources/terrains/wave/wave_1.xml new file mode 100644 index 0000000..6d5a73f --- /dev/null +++ b/resources/terrains/wave/wave_1.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_10.xml b/resources/terrains/wave/wave_10.xml new file mode 100644 index 0000000..b4b3861 --- /dev/null +++ b/resources/terrains/wave/wave_10.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_2.xml b/resources/terrains/wave/wave_2.xml new file mode 100644 index 0000000..e557a54 --- /dev/null +++ b/resources/terrains/wave/wave_2.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_3.xml b/resources/terrains/wave/wave_3.xml new file mode 100644 index 0000000..5fc39c0 --- /dev/null +++ b/resources/terrains/wave/wave_3.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_4.xml b/resources/terrains/wave/wave_4.xml new file mode 100644 index 0000000..e622e68 --- /dev/null +++ b/resources/terrains/wave/wave_4.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_5.xml b/resources/terrains/wave/wave_5.xml new file mode 100644 index 0000000..675b2fd --- /dev/null +++ b/resources/terrains/wave/wave_5.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_6.xml b/resources/terrains/wave/wave_6.xml new file mode 100644 index 0000000..0ea2785 --- /dev/null +++ b/resources/terrains/wave/wave_6.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_7.xml b/resources/terrains/wave/wave_7.xml new file mode 100644 index 0000000..48f5dce --- /dev/null +++ b/resources/terrains/wave/wave_7.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_8.xml b/resources/terrains/wave/wave_8.xml new file mode 100644 index 0000000..b62af90 --- /dev/null +++ b/resources/terrains/wave/wave_8.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/terrains/wave/wave_9.xml b/resources/terrains/wave/wave_9.xml new file mode 100644 index 0000000..4822108 --- /dev/null +++ b/resources/terrains/wave/wave_9.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py index 10f7418..49f6676 100644 --- a/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py +++ b/robogauge/tasks/gauge/gauge_configs/terrain_levels_config.py @@ -22,5 +22,16 @@ class TerrainLevelsConfig(Config): class wave: levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - targets = [] + targets = [ + [6.5, 0.0, 0.08], + [6.5, 0.0, 0.16], + [6.5, 0.0, 0.24], + [6.5, 0.0, 0.32], + [6.5, 0.0, 0.40], + [6.5, 0.0, 0.48], + [6.5, 0.0, 0.56], + [6.5, 0.0, 0.64], + [6.5, 0.0, 0.72], + [6.5, 0.0, 0.80], + ] diff --git a/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py index 461c474..3f84555 100644 --- a/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py +++ b/robogauge/tasks/gauge/gauge_configs/wave_gauge_config.py @@ -14,14 +14,14 @@ class WaveGaugeConfig(BaseGaugeConfig): class assets(BaseGaugeConfig.assets): 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 + terrain_level = 10 # 1-10 + terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/wave/wave_10.xml' + terrain_spawn_pos = [1.5, 1.25, 1.0] # x y z [m], robot freejoint spawn position on the terrain class goals: class target_pos_velocity: # goal to reach a target position by velocity command enabled = True - target_pos = [4, 0, 1.0] # x y z [m], target position in the environment, used for target position goal + target_pos = [6.5, 0.0, 0.8] # 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/pipeline/base_pipeline.py b/robogauge/tasks/pipeline/base_pipeline.py index ad6fb7d..bb9029b 100644 --- a/robogauge/tasks/pipeline/base_pipeline.py +++ b/robogauge/tasks/pipeline/base_pipeline.py @@ -117,7 +117,7 @@ class BasePipeline: logger.info("✅ Pipeline execution finished.") logger.info(f"📁 Logging saved at: {logger.log_dir}") - return logger.log_dir, error + return self.gauge.results, error def reset_sim_and_robot(self, sim_data: SimData): self.sim.reset() diff --git a/robogauge/tasks/pipeline/level_pipeline.py b/robogauge/tasks/pipeline/level_pipeline.py index c9c1b66..cb56126 100644 --- a/robogauge/tasks/pipeline/level_pipeline.py +++ b/robogauge/tasks/pipeline/level_pipeline.py @@ -16,7 +16,6 @@ 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): @@ -25,30 +24,39 @@ class LevelPipeline: # binary search levels l, r = 0, 10 + all_level_results = {} while l < r: level = (l + r + 1) // 2 - if self.test_level(level): + all_success, results = self.test_level(level) + all_level_results[level] = results + if all_success: l = level else: r = level - 1 - if l >= 1: - logger.info(f"🏆 Found maximum level: {l}") + level = l + level_results = all_level_results.get(l, { + 'model_path': results['model_path'], + 'terrain_name': results['terrain_name'], + 'terrain_level': 0, + }) + if level >= 1: + 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: + 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}...") 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]) + aggregated_results = multi_pipeline.run() + success_mean = float(aggregated_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 + return all_success, aggregated_results \ No newline at end of file diff --git a/robogauge/tasks/pipeline/multi_pipeline.py b/robogauge/tasks/pipeline/multi_pipeline.py index b64d0ae..7c88437 100644 --- a/robogauge/tasks/pipeline/multi_pipeline.py +++ b/robogauge/tasks/pipeline/multi_pipeline.py @@ -36,18 +36,19 @@ def run_single_process(args, data): console_output=False ) pipeline = task_register.make_pipeline(args=local_args, create_logger=False) - log_dir, error = pipeline.run() + results, error = pipeline.run() if error is None: ret = { 'status': 'success', - 'log_dir': log_dir, + 'results': results, 'model_path': pipeline.robot_cfg.control.model_path, } else: logger.error(f"❌ Process with seed={seed}, base_mass={base_mass}, friction={friction} failed with error: {error}") ret = { 'status': 'error', - 'log_dir': log_dir, + 'results': results, + 'model_path': pipeline.robot_cfg.control.model_path, 'data': data, 'error_msg': str(error), 'traceback': traceback.format_exc() @@ -61,8 +62,14 @@ class MultiPipeline: self.frictions = args.frictions self.base_masses = args.base_masses self.num_processes = args.num_processes - self.model_path = None + self.static_info = {} 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: + 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): logger.info(f"🚀 Starting Multi-Process Evaluation with {self.num_processes} processes.") @@ -71,32 +78,30 @@ class MultiPipeline: 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) - result_log_dirs = [] + 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') - result_log_dirs.append(results['log_dir']) - if results['status'] == 'success': - if self.model_path is None: - self.model_path = results['model_path'] - else: - assert self.model_path == results['model_path'], "Model paths do not match across runs." - else: + results_list.append(results['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']}") logger.info("✅ Multi-Process Evaluation Completed.") - self.aggregate_results(result_log_dirs, success_flags, workers_data) - return logger.log_dir + aggregated_results = self.aggregate_results(results_list, success_flags, workers_data) + return aggregated_results - def aggregate_results(self, log_dirs, success_flags, workers_data): - """ Process results.yaml from each log_dir """ + def aggregate_results(self, all_results, success_flags, workers_data): + """ Process results from all processes and aggregate them. """ logger.info("📊 Aggregating Results from all runs...") - summary = {'model_path': self.model_path, 'success': {}} + 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""" @@ -109,27 +114,9 @@ class MultiPipeline: finish_msg += f"""{'='*88}""" logger.info(finish_msg) - all_results = [] - all_yaml_paths = [] - for path in log_dirs: - yaml_path = Path(path) / "results.yaml" - if not yaml_path.exists(): - logger.warning(f"Results file not found: {yaml_path}, skipping.") - continue - with open(yaml_path, 'r') as file: - data = yaml.safe_load(file) - if data: - all_results.append(data) - all_yaml_paths.append(yaml_path) if not all_results: logger.error("No results to aggregate.") return - yaml_paths_str = '\n'.join([str(p) for p in all_yaml_paths]) - logger.info( - f"""\n{'='*20} Results Files {'='*20}\n""" - f"""{yaml_paths_str}\n""" - f"""{'='*56}""" - ) value_collections = defaultdict(lambda: defaultdict(list)) for result in all_results: @@ -156,3 +143,4 @@ class MultiPipeline: f"""{yaml.dump(summary, allow_unicode=True)}""" f"""{'='*60}""" ) + return summary