From 939dc69b7e15d18a4ed5fceb1b3c819eb839f6c2 Mon Sep 17 00:00:00 2001 From: wty-yy <993660140@qq.com> Date: Thu, 18 Dec 2025 23:14:18 +0800 Subject: [PATCH] v0.1.10 --- README.md | 9 +- UPDATE.md | 7 + robogauge/scripts/run_eval_models.sh | 8 +- robogauge/tasks/custom/go2_flat_task.py | 21 +- robogauge/tasks/gauge/base_gauge.py | 8 +- robogauge/tasks/gauge/base_gauge_config.py | 14 +- .../gauge/gauge_configs/flat_gauge_config.py | 2 +- robogauge/tasks/gauge/metrics/__init__.py | 3 +- robogauge/tasks/gauge/metrics/base_metric.py | 12 ++ robogauge/tasks/gauge/metrics/dof_metrics.py | 35 ++++ .../tasks/gauge/metrics/stable_metric.py | 61 ++++++ robogauge/tasks/gauge/metrics/vel_metrics.py | 9 + .../tasks/gauge/metrics/visualization.py | 21 +- robogauge/tasks/pipeline/base_pipeline.py | 3 + robogauge/tasks/pipeline/multi_pipeline.py | 2 +- robogauge/tasks/robots/base_robot.py | 16 -- robogauge/tasks/robots/go2/go2.py | 3 +- robogauge/tasks/robots/go2/go2_moe.py | 5 - robogauge/tasks/simulator/mujoco_simulator.py | 4 +- robogauge/tasks/simulator/sim_data.py | 2 +- robogauge/utils/helpers.py | 2 + robogauge/utils/logger.py | 11 +- robogauge/utils/math_utils.py | 18 ++ robogauge/utils/radar_plot.py | 183 ++++++++++++++++++ robogauge/utils/task_register.py | 2 + 25 files changed, 411 insertions(+), 50 deletions(-) create mode 100644 robogauge/tasks/gauge/metrics/stable_metric.py create mode 100644 robogauge/utils/math_utils.py create mode 100644 robogauge/utils/radar_plot.py diff --git a/README.md b/README.md index 6537350..f9861a0 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,10 @@ | - | - | - | | 电机动作执行随机延迟 | `action delay` | `<= RL控制间隔` | | base负重 | `base mass` | `-1, 0, 1, 2, 3 kg` | +| 地面摩擦力 | `friction` | `0.4, 0.7, 1.0, 1.3, 1.6` | #### 地形 -1. 支持legged_gym中的部分地形, 包括: `wave, slope, rough_slope, stairs up, stairs down, obstacles, flat`, 除`flat`地形外其他地形可进行难度系数提升 +1. 支持legged_gym中的部分地形, 包括: `wave, slope, stairs up, stairs down, obstacles, flat`, 除`flat`地形外其他地形可进行难度系数提升 2. 地面类型 (影响接触摩擦系数, 弹性摩擦系数), 包括: 橡胶地, 木地板, 瓷砖地 ### 指标 @@ -41,8 +42,9 @@ | 1 | `dof_limits` | 关节超出软关节范围的大小 | 软关节范围阈值 | 总关节变化范围 | `1-x` | | 2 | `lin_vel_err` | 线速度L2误差 | NA | 总线速度指令范围 | `1-x` | | 3 | `ang_vel_err` | 角速度L2误差 | NA | 总角速度指令范围 | `1-x` | -| 4 | `base_height_std` | base高度变化方差 | NA | NA | `1-x` | -| 5 | `dof_power` | 电机耗能 | NA | 10 | `1-x` | +| 4 | `dof_power` | 电机耗能 | 缩放系数 | 100 | `1-x` | +| 5 | `orientation_stability` | 机身姿态稳定性 (Roll) | NA | NA | `1-x` | +| 6 | `torque_smoothness` | 力矩平滑度 | 缩放系数 | 30 | `1-x` | ### 速度追踪目标 针对在虚实迁移中发现的问题, 整理指标 (metrics) 内容如下: @@ -58,6 +60,7 @@ 总结速度最总目标 (goals) 如下: | # | 目标名称 Goals | 描述 | reset条件 | 最大reset次数 | +| - | - | - | - | - | | 1 | `max_velocity` | 单一维度的最大线/角速度 | 每次执行一个方向的指令, 再急停 | 6 | | 2 | `diagonal_velocity` | 对角线速度变化 | 每次执行一对对角指令 | 8 | diff --git a/UPDATE.md b/UPDATE.md index 5776608..7452410 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -1,5 +1,12 @@ # UPDATE +TODO: 在模型崩溃时也记录下最后的gauge信息 ## 20251218 +### v0.1.10 +1. 加入`--write-tensorboard`参数, 默认为`False`即不记录`gauge`的日志信息 +2. 指令系数改为1.8 (比2.0稳定点) +3. 完成全部指标, 新增`dof_power, orientation_stability, torque_smoothness` +4. 加入雷达图绘图 +Fix Bugs: 日志记录重复的问题 ### v0.1.9 1. 在`MaxVelocityGoal`基础上加入`end_stance`, 最终保持站立姿态 2. 在开始goal控制前, 先等机器人落地, 通过线速度小于0.05阈值判断静止后, 执行goal diff --git a/robogauge/scripts/run_eval_models.sh b/robogauge/scripts/run_eval_models.sh index 2009f1e..5b98632 100755 --- a/robogauge/scripts/run_eval_models.sh +++ b/robogauge/scripts/run_eval_models.sh @@ -15,7 +15,7 @@ MODELS_FILE="$SCRIPT_DIR/evaluate_models.txt" RUN_PY="$SCRIPT_DIR/run.py" ### Default Configure ### -EXP_NAME="go2_moe_flat" # Experiment name [-n] +EXP_NAME="" # Experiment name [-n] TASK_NAME="go2_moe_flat" # Task name [-t] SAVE_VIDEO=false # Whether to save video [-s] @@ -53,12 +53,16 @@ if [ ${#models_paths[@]} -eq 0 ]; then fi ### Run Evaluation Scripts ### -base_args="--task $TASK_NAME --headless --experiment-name $EXP_NAME" +base_args="--task $TASK_NAME --headless" if [ "$SAVE_VIDEO" = true ]; then base_args="$base_args --save-video" fi +if [ -n "$EXP_NAME" ]; then + base_args="$base_args --exp-name $EXP_NAME" +fi + echo "================ Run Settings ================" echo "Script Dir: $SCRIPT_DIR" echo "Runner: $RUN_PY" diff --git a/robogauge/tasks/custom/go2_flat_task.py b/robogauge/tasks/custom/go2_flat_task.py index e6edbe2..1047c8a 100644 --- a/robogauge/tasks/custom/go2_flat_task.py +++ b/robogauge/tasks/custom/go2_flat_task.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +''' +@File : go2_flat_task.py +@Time : 2025/12/18 20:19:25 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Go2 Flat Task Configuration +''' from robogauge.tasks.robots import Go2Config, Go2MoEConfig from robogauge.tasks.gauge import FlatGaugeConfig from robogauge.tasks.simulator.mujoco_config import MujocoConfig @@ -22,15 +31,15 @@ class Go2FlatGaugeConfig(FlatGaugeConfig): class Go2FlatConfig(Go2Config): class commands(Go2Config.commands): - lin_vel_x = [-2.0, 2.0] # min max [m/s] - lin_vel_y = [-2.0, 2.0] # min max [m/s] - ang_vel_yaw = [-2.0, 2.0] # min max [rad/s] + lin_vel_x = [-1.8, 1.8] # min max [m/s] + lin_vel_y = [-1.8, 1.8] # min max [m/s] + ang_vel_yaw = [-1.8, 1.8] # min max [rad/s] class Go2MoEFlatConfig(Go2MoEConfig): class commands(Go2Config.commands): - lin_vel_x = [-2.0, 2.0] # min max [m/s] - lin_vel_y = [-2.0, 2.0] # min max [m/s] - ang_vel_yaw = [-2.0, 2.0] # min max [rad/s] + lin_vel_x = [-1.8, 1.8] # min max [m/s] + lin_vel_y = [-1.8, 1.8] # min max [m/s] + ang_vel_yaw = [-1.8, 1.8] # min max [rad/s] class control(Go2Config.control): # model_path = "{ROBOGAUGE_ROOT_DIR}/resources/models/go2/go2_moe_cts_124k.pt" diff --git a/robogauge/tasks/gauge/base_gauge.py b/robogauge/tasks/gauge/base_gauge.py index a3781e8..d95a843 100644 --- a/robogauge/tasks/gauge/base_gauge.py +++ b/robogauge/tasks/gauge/base_gauge.py @@ -26,7 +26,10 @@ from robogauge.tasks.gauge.goals import BaseGoal, MaxVelocityGoal, DiagonalVeloc from robogauge.tasks.gauge.metrics import * class BaseGauge: - def __init__(self, cfg: BaseGaugeConfig, robot_cfg: RobotConfig): + def __init__(self, + cfg: BaseGaugeConfig, + robot_cfg: RobotConfig, + ): self.cfg = cfg self.robot_cfg = robot_cfg self.goals_cfg = class_to_dict(self.cfg.goals) @@ -79,7 +82,8 @@ class BaseGauge: def create_new_goal_logger(self): """ Create a new logger for new goal to metrics. """ - if self.goal_idx >= len(self.goals): return + if self.goal_idx >= len(self.goals) or not self.cfg.write_tensorboard: + return logger.create_tensorboard( self.robot_cfg.robot_name, Path(self.robot_cfg.control.model_path).stem, diff --git a/robogauge/tasks/gauge/base_gauge_config.py b/robogauge/tasks/gauge/base_gauge_config.py index 1bf338b..4a2f435 100644 --- a/robogauge/tasks/gauge/base_gauge_config.py +++ b/robogauge/tasks/gauge/base_gauge_config.py @@ -11,6 +11,7 @@ from robogauge.utils.config import Config class BaseGaugeConfig(Config): gauge_class = 'BaseGauge' + write_tensorboard = False # Whether to write tensorboard logs class assets: terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/flat.xml' @@ -34,7 +35,7 @@ class BaseGaugeConfig(Config): class visualization: enabled = True - dof_force = True + dof_torque = True dof_pos = True class lin_vel_err: @@ -42,3 +43,14 @@ class BaseGaugeConfig(Config): class ang_vel_err: enabled = True + + class dof_power: + enabled = True + scaling_factor = 100.0 # [W] scaling factor for power metric + + class orientation_stability: + enabled = True + + class torque_smoothness: + enabled = True + scaling_factor = 30.0 # [Nm] scaling factor for torque smoothness metric diff --git a/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py b/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py index f49b7c6..55cc2a3 100644 --- a/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py +++ b/robogauge/tasks/gauge/gauge_configs/flat_gauge_config.py @@ -36,7 +36,7 @@ class FlatGaugeConfig(BaseGaugeConfig): class visualization: enabled = True - dof_force = True + dof_torque = True dof_pos = True class lin_vel_err: diff --git a/robogauge/tasks/gauge/metrics/__init__.py b/robogauge/tasks/gauge/metrics/__init__.py index 469cb4d..137426b 100644 --- a/robogauge/tasks/gauge/metrics/__init__.py +++ b/robogauge/tasks/gauge/metrics/__init__.py @@ -1,4 +1,5 @@ from .base_metric import BaseMetric -from .dof_metrics import DofLimitsMetric +from .dof_metrics import DofLimitsMetric, DofPowerMetric from .visualization import VisualizationMetric from .vel_metrics import LinVelErrMetric, AngVelErrMetric +from .stable_metric import OrientationStabilityMetric, TorqueSmoothnessMetric diff --git a/robogauge/tasks/gauge/metrics/base_metric.py b/robogauge/tasks/gauge/metrics/base_metric.py index 83f723f..b11ea0d 100644 --- a/robogauge/tasks/gauge/metrics/base_metric.py +++ b/robogauge/tasks/gauge/metrics/base_metric.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +''' +@File : base_metric.py +@Time : 2025/12/18 20:18:45 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Base Metric Implementation +''' from robogauge.utils.logger import logger from robogauge.tasks.robots import RobotConfig from robogauge.tasks.simulator.sim_data import SimData @@ -9,6 +18,9 @@ class BaseMetric: def __init__(self, robot_cfg: RobotConfig, **kwargs): self.robot_cfg = robot_cfg + + def reset(self): + pass def __call__(self, sim_data: SimData, goal_data: GoalData) -> float: value = 0.0 diff --git a/robogauge/tasks/gauge/metrics/dof_metrics.py b/robogauge/tasks/gauge/metrics/dof_metrics.py index ae46fd9..bc4816b 100644 --- a/robogauge/tasks/gauge/metrics/dof_metrics.py +++ b/robogauge/tasks/gauge/metrics/dof_metrics.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +''' +@File : dof_metrics.py +@Time : 2025/12/18 20:18:24 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : DOF Limits Metric Implementation +''' import numpy as np from robogauge.tasks.robots import RobotConfig @@ -47,3 +56,29 @@ class DofLimitsMetric(BaseMetric): rms_value = 1 - np.sqrt(np.mean(np.square(values))) logger.log(1 - rms_value, f'dof_limits/rms', step=sim_data.n_step) return rms_value + +class DofPowerMetric(BaseMetric): + """ Metric to log DOF power efficiency. """ + name = 'dof_power_metric' + + def __init__(self, + robot_cfg: RobotConfig, + scaling_factor: float = 100.0, + **kwargs + ): + super().__init__(robot_cfg) + self.scaling_factor = scaling_factor + + def __call__(self, sim_data: SimData, goal_data: GoalData) -> float: + values = [] + for i in range(len(sim_data.proprio.joint.torque)): + torque = sim_data.proprio.joint.torque[i] + velocity = sim_data.proprio.joint.vel[i] + power = abs(torque * velocity) + values.append(power) + dof_name = sim_data.proprio.joint.names[i] + logger.log(power, f'dof_power/{dof_name}', step=sim_data.n_step) + rms_power = np.sqrt(np.mean(np.square(values))) + metric_power = 1 - rms_power / self.scaling_factor + logger.log(rms_power, f'dof_power/rms', step=sim_data.n_step) + return metric_power diff --git a/robogauge/tasks/gauge/metrics/stable_metric.py b/robogauge/tasks/gauge/metrics/stable_metric.py new file mode 100644 index 0000000..024a3fb --- /dev/null +++ b/robogauge/tasks/gauge/metrics/stable_metric.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +''' +@File : height_metric.py +@Time : 2025/12/18 20:18:33 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Orientation Stability, Torque Smoothness Metric Implementation +''' +import numpy as np + +from robogauge.tasks.robots import RobotConfig +from robogauge.tasks.gauge.metrics.base_metric import BaseMetric, GoalData, SimData +from robogauge.utils.math_utils import get_projected_gravity + +from robogauge.utils.logger import logger + + +class OrientationStabilityMetric(BaseMetric): + """ Metric to log height stability. """ + name = 'height_std_metric' + + def __init__(self, + robot_cfg: RobotConfig, + **kwargs + ): + super().__init__(robot_cfg) + + def __call__(self, sim_data: SimData, goal_data: GoalData) -> float: + projected_gravity = get_projected_gravity(sim_data.proprio.base.quat) + projected_x = projected_gravity[0] + metric_value = 1 - abs(projected_x) # consider roll only + logger.log(abs(projected_x), f'stable_metric/projected_x_abs', step=sim_data.n_step) + return metric_value + +class TorqueSmoothnessMetric(BaseMetric): + """ Metric to log torque smoothness. """ + name = 'torque_smoothness_metric' + + def __init__(self, + robot_cfg: RobotConfig, + scaling_factor: float = 30.0, + **kwargs + ): + super().__init__(robot_cfg) + self.last_torque = None + self.scaling_factor = scaling_factor + + def reset(self): + self.last_torque = None + + def __call__(self, sim_data: SimData, goal_data: GoalData) -> float: + current_torque = np.array(sim_data.proprio.joint.torque, np.float32) + if self.last_torque is None: + self.last_torque = current_torque + return 1.0 # No change at first step + torque_diff = current_torque - self.last_torque + rms_value = np.sqrt(np.mean(np.square(torque_diff))) + metric_value = 1.0 - rms_value / self.scaling_factor + logger.log(rms_value, f'stable_metric/torque_rms_diff', step=sim_data.n_step) + return metric_value \ No newline at end of file diff --git a/robogauge/tasks/gauge/metrics/vel_metrics.py b/robogauge/tasks/gauge/metrics/vel_metrics.py index 1f75014..b1f9fcd 100644 --- a/robogauge/tasks/gauge/metrics/vel_metrics.py +++ b/robogauge/tasks/gauge/metrics/vel_metrics.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +''' +@File : vel_metrics.py +@Time : 2025/12/18 20:18:56 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Velocity Metrics Implementation +''' import numpy as np from robogauge.tasks.robots import RobotConfig diff --git a/robogauge/tasks/gauge/metrics/visualization.py b/robogauge/tasks/gauge/metrics/visualization.py index 5ec003b..b5a02c2 100644 --- a/robogauge/tasks/gauge/metrics/visualization.py +++ b/robogauge/tasks/gauge/metrics/visualization.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +''' +@File : visualization.py +@Time : 2025/12/18 20:19:09 +@Author : wty-yy +@Version : 1.0 +@Blog : https://wty-yy.github.io/ +@Desc : Visualization Metric Implementation +''' from robogauge.tasks.robots import RobotConfig from robogauge.tasks.gauge.metrics.base_metric import BaseMetric, SimData, GoalData @@ -9,20 +18,20 @@ class VisualizationMetric(BaseMetric): def __init__(self, robot_cfg: RobotConfig, - dof_force: bool = False, + dof_torque: bool = False, dof_pos: bool = False, **kwargs ): super().__init__(robot_cfg) - self.dof_force = dof_force + self.dof_torque = dof_torque self.dof_pos = dof_pos def __call__(self, sim_data: SimData, goal_data: GoalData) -> float: - for i in range(len(sim_data.proprio.joint.force)): + for i in range(len(sim_data.proprio.joint.torque)): name = sim_data.proprio.joint.names[i] - if self.dof_force: - force = sim_data.proprio.joint.force[i] - logger.log(force, f'dof_force/{name}', step=sim_data.n_step) + if self.dof_torque: + torque = sim_data.proprio.joint.torque[i] + logger.log(torque, f'dof_torque/{name}', step=sim_data.n_step) if self.dof_pos: pos = sim_data.proprio.joint.pos[i] logger.log(pos, f'dof_pos/{name}', step=sim_data.n_step) diff --git a/robogauge/tasks/pipeline/base_pipeline.py b/robogauge/tasks/pipeline/base_pipeline.py index ab00167..9d16472 100644 --- a/robogauge/tasks/pipeline/base_pipeline.py +++ b/robogauge/tasks/pipeline/base_pipeline.py @@ -100,6 +100,9 @@ class BasePipeline: first_reset = True self.last_reset_time = sim_data.sim_time sim_data = self.sim.step() + except Exception as e: + logger.error(f"❌ Pipeline execution failed with error: {e}") + raise e finally: self.sim.close_viewer() self.sim.close_video_writer() diff --git a/robogauge/tasks/pipeline/multi_pipeline.py b/robogauge/tasks/pipeline/multi_pipeline.py index 36258de..482453b 100644 --- a/robogauge/tasks/pipeline/multi_pipeline.py +++ b/robogauge/tasks/pipeline/multi_pipeline.py @@ -144,7 +144,7 @@ class MultiPipeline: save_path = logger.log_dir / "aggregated_results.yaml" with open(save_path, 'w') as file: - yaml.dump(summary, file, allow_unicode=True) + yaml.dump(summary, file, allow_unicode=True, sort_keys=False) logger.info("✅ Aggregated execution finished.") logger.info(f"📁 Aggregated results saved to: {save_path}") diff --git a/robogauge/tasks/robots/base_robot.py b/robogauge/tasks/robots/base_robot.py index f8abd34..0cd1d8e 100644 --- a/robogauge/tasks/robots/base_robot.py +++ b/robogauge/tasks/robots/base_robot.py @@ -45,19 +45,3 @@ class BaseRobot: action = np.zeros(self.num_action, dtype=np.float32) return action, self.p_gains, self.d_gains, self.control_type -def get_projected_gravity(quat): - """ Compute world frame gravity (0, 0, -1) projected into robot base frame. - Args: - quat: (4,) quaternion (w, x, y, z) from robot base to world frame - Returns: - projected_gravity: (3,) projected gravity vector in robot base frame - """ - qw, qx, qy, qz = quat - - gravity_orientation = np.zeros(3) - - gravity_orientation[0] = 2 * (-qz * qx + qw * qy) - gravity_orientation[1] = -2 * (qz * qy + qw * qx) - gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz) - - return gravity_orientation diff --git a/robogauge/tasks/robots/go2/go2.py b/robogauge/tasks/robots/go2/go2.py index 89c1bdb..3b93b94 100644 --- a/robogauge/tasks/robots/go2/go2.py +++ b/robogauge/tasks/robots/go2/go2.py @@ -10,7 +10,8 @@ import torch import numpy as np -from robogauge.tasks.robots.base_robot import BaseRobot, get_projected_gravity +from robogauge.tasks.robots.base_robot import BaseRobot +from robogauge.utils.math_utils import get_projected_gravity from robogauge.tasks.robots.go2.go2_config import Go2Config from robogauge.tasks.simulator.sim_data import SimData from robogauge.tasks.gauge.goal_data import GoalData diff --git a/robogauge/tasks/robots/go2/go2_moe.py b/robogauge/tasks/robots/go2/go2_moe.py index 6f8943e..67015ac 100644 --- a/robogauge/tasks/robots/go2/go2_moe.py +++ b/robogauge/tasks/robots/go2/go2_moe.py @@ -10,12 +10,7 @@ import torch import numpy as np -from robogauge.tasks.robots.base_robot import BaseRobot, get_projected_gravity -from robogauge.tasks.robots.go2.go2_config import Go2Config from robogauge.tasks.robots.go2.go2 import Go2 -from robogauge.tasks.simulator.sim_data import SimData -from robogauge.tasks.gauge.goal_data import GoalData -from robogauge.utils.logger import logger class Go2MoE(Go2): def get_action(self, obs: np.ndarray): diff --git a/robogauge/tasks/simulator/mujoco_simulator.py b/robogauge/tasks/simulator/mujoco_simulator.py index c0f01a0..ec0f5c1 100644 --- a/robogauge/tasks/simulator/mujoco_simulator.py +++ b/robogauge/tasks/simulator/mujoco_simulator.py @@ -200,7 +200,7 @@ class MujocoSimulator: joint=JointState( pos=self.get_sensor_data('joint_pos'), vel=self.get_sensor_data('joint_vel'), - force=self.get_sensor_data('joint_eff'), + torque=self.get_sensor_data('joint_eff'), limits=self.dof_limits, names=self.dof_names, ), @@ -389,7 +389,7 @@ class MujocoSimulator: logger.info("Proprioception shapes:") logger.info(f" joint.pos: { _shape(jp.pos) }") logger.info(f" joint.vel: { _shape(jp.vel) }") - logger.info(f" joint.force: { _shape(jp.force) }") + logger.info(f" joint.torque: { _shape(jp.torque) }") logger.info(f" base.pos: { _shape(bs.pos) }") logger.info(f" base.quat: { _shape(bs.quat) }") diff --git a/robogauge/tasks/simulator/sim_data.py b/robogauge/tasks/simulator/sim_data.py index d3b0893..6bfd3d1 100644 --- a/robogauge/tasks/simulator/sim_data.py +++ b/robogauge/tasks/simulator/sim_data.py @@ -5,7 +5,7 @@ from dataclasses import dataclass class JointState: pos: np.ndarray # [rad] shape (n_dof,) vel: np.ndarray # [rad/s] shape (n_dof,) - force: np.ndarray # [N*m] shape (n_dof,) + torque: np.ndarray # [N*m] shape (n_dof,) limits: np.ndarray # [rad] shape (n_dof, 2), lower and upper limits names: list # list of joint names diff --git a/robogauge/utils/helpers.py b/robogauge/utils/helpers.py index b878959..98515ab 100644 --- a/robogauge/utils/helpers.py +++ b/robogauge/utils/helpers.py @@ -71,6 +71,8 @@ def parse_args(): {"name": "--headless", "action": "store_true", "default": False, "help": "Run in headless mode."}, {"name": "--save-video", "action": "store_true", "default": False, "help": "Save video output."}, {"name": "--seed", "type": int, "default": 42, "help": "Random seed."}, + {"name": "--write-tensorboard", "action": "store_true", "default": False, "help": "Write tensorboard logs."}, + {"name": "--plot-radar", "action": "store_true", "default": False, "help": "Plot radar charts for metrics."}, # Multiprocessing parameters, with different seeds {"name": "--multi", "action": "store_true", "default": False, "help": "Enable multiprocessing."}, diff --git a/robogauge/utils/logger.py b/robogauge/utils/logger.py index b22ed78..74f72bd 100644 --- a/robogauge/utils/logger.py +++ b/robogauge/utils/logger.py @@ -76,6 +76,13 @@ class Logger: self.logger = logging.getLogger(experiment_name + "_logger") self.logger.setLevel(log_level) self.logger.propagate = False + + # Clear existing handlers to prevent duplicate logging + if self.logger.hasHandlers(): + for handler in self.logger.handlers[:]: + handler.close() + self.logger.removeHandler(handler) + self.time_tag = time.strftime("%Y%m%d-%H-%M-%S") self.tag = f"{self.time_tag}_{run_name}" self.experiment_name = experiment_name @@ -141,8 +148,8 @@ class Logger: """ if self.writer is not None: self.writer.add_scalar(tag, value, step) - else: - self.warning("Tensorboard writer is not initialized, skipping log.") + # else: + # self.warning("Tensorboard writer is not initialized, skipping log.") logger = Logger() diff --git a/robogauge/utils/math_utils.py b/robogauge/utils/math_utils.py new file mode 100644 index 0000000..48e349e --- /dev/null +++ b/robogauge/utils/math_utils.py @@ -0,0 +1,18 @@ +import numpy as np + +def get_projected_gravity(quat): + """ Compute world frame gravity (0, 0, -1) projected into robot base frame. + Args: + quat: (4,) quaternion (w, x, y, z) from robot base to world frame + Returns: + projected_gravity: (3,) projected gravity vector in robot base frame + """ + qw, qx, qy, qz = quat + + gravity_orientation = np.zeros(3) + + gravity_orientation[0] = 2 * (-qz * qx + qw * qy) + gravity_orientation[1] = -2 * (qz * qy + qw * qx) + gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz) + + return gravity_orientation diff --git a/robogauge/utils/radar_plot.py b/robogauge/utils/radar_plot.py new file mode 100644 index 0000000..14b8a90 --- /dev/null +++ b/robogauge/utils/radar_plot.py @@ -0,0 +1,183 @@ +""" +python robogauge/utils/radar_plot.py \ + /home/xfy/Coding/robot_gauge/logs/go2_moe_flat_debug_multi/20251218-22-49-29_run_multi/aggregated_results.yaml \ + /home/xfy/Coding/robot_gauge/logs/go2_flat_debug_multi/20251218-22-59-04_run_multi/aggregated_results.yaml \ + --out logs/go2_flat_vs_moe_flat.png +""" +import matplotlib.pyplot as plt +config = { + "font.family": 'serif', # 衬线字体 + "figure.figsize": (6, 6), # 图像大小 + "font.size": 14, # 字号大小 + "mathtext.fontset": 'cm', # 渲染数学公式字体 + 'axes.unicode_minus': False # 显示负号 +} +plt.rcParams.update(config) + +import numpy as np +import yaml +import argparse +import os +import sys + +# 设置字体,尝试匹配参考图的衬线体风格 (如果系统没有会回退到默认) +plt.rcParams['font.family'] = 'serif' +plt.rcParams['font.serif'] = ['Times New Roman', 'DejaVu Serif', 'serif'] + +def parse_value_string(val_str): + if isinstance(val_str, (int, float)): + return float(val_str) + if isinstance(val_str, str): + if '±' in val_str: + return float(val_str.split('±')[0].strip()) + return float(val_str) + return 0.0 + +def load_data(file_paths): + all_data = [] + + # 指标键名 + metric_keys = [ + 'lin_vel_err', + 'ang_vel_err', + 'orientation_stability', + 'dof_limits', + 'torque_smoothness', + 'dof_power' + ] + + # 标签 (增加换行以避免拥挤) + labels_map = { + 'lin_vel_err': 'Lin Vel\nAccuracy', + 'ang_vel_err': 'Ang Vel\nAccuracy', + 'dof_limits': 'Joint Limits\nMargin', + 'dof_power': 'Energy\nEfficiency', + 'orientation_stability': 'Orientation\nStability', + 'torque_smoothness': 'Torque\nSmoothness', + } + + for path in file_paths: + if not os.path.exists(path): + continue + + with open(path, 'r', encoding='utf-8') as f: + content = yaml.safe_load(f) + + raw_path = content.get('model_path', 'Unknown_Model') + # 简化图例名称:只取文件名,去掉 .pt + model_name = os.path.basename(raw_path).replace('.pt', '') + + # 如果名称过长,可以考虑进一步截断,例如: + # if len(model_name) > 20: model_name = model_name[:10] + "..." + model_name[-5:] + + values = [] + for k in metric_keys: + if k in content: + raw_val = content[k]['mean'] + values.append(parse_value_string(raw_val)) + else: + values.append(0.0) + + all_data.append({'name': model_name, 'values': values}) + + return all_data, [labels_map[k] for k in metric_keys] + +def plot_radar(data_list, labels, output_file=None): + if not data_list: + print("No data to plot.") + return + + num_vars = len(labels) + angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist() + angles += angles[:1] # 闭合 + + # --- 颜色设置 --- + # 使用参考图类似的配色 (深蓝、浅蓝、绿等) + # 或者使用 'tab10', 'Set2' 等 + colors = plt.cm.get_cmap("tab10", len(data_list)) + + # 创建画布,稍微宽一点以便放图例 + fig, ax = plt.subplots(figsize=(10, 8), subplot_kw=dict(polar=True)) + + # --- 核心修改:调整布局 --- + # left=0.1, bottom=0.1, top=0.9 是为了给标题留空 + # right=0.75 是关键!这意味着图表只占画布左边 75% 的宽度,右边 25% 留给图例 + plt.subplots_adjust(left=0.05, right=0.75, top=0.9, bottom=0.1) + + # 设置方向 + ax.set_theta_offset(np.pi / 2) + ax.set_theta_direction(-1) + + # --- 绘制标签 --- + plt.xticks(angles[:-1], labels, color='#444444', size=13) + + # 标签对齐优化 + for label, angle in zip(ax.get_xticklabels(), angles[:-1]): + if angle in (0, np.pi): + label.set_horizontalalignment('center') + elif 0 < angle < np.pi: + label.set_horizontalalignment('left') + else: + label.set_horizontalalignment('right') + + # --- 绘制刻度 --- + ax.set_rlabel_position(0) + # 字体稍微调淡一点,不要抢眼 + plt.yticks([0.25, 0.50, 0.75, 1.00], ["0.25", "0.50", "0.75", "1.00"], + color="grey", size=10) + plt.ylim(0, 1.05) + + # 网格线:点状虚线,稍微粗一点 + ax.grid(True, color='gray', linestyle=':', linewidth=1.5, alpha=0.5) + ax.spines['polar'].set_visible(False) + + # --- 绘制数据 --- + # 加粗线条以匹配 bsuite 风格 + linewidth = 3.0 + + for idx, item in enumerate(data_list): + values = item['values'] + name = item['name'] + values_closed = values + values[:1] + + color = colors(idx) + + ax.plot(angles, values_closed, linewidth=linewidth, linestyle='-', label=name, color=color) + ax.fill(angles, values_closed, color=color, alpha=0.2) # 填充透明度低一点 + + # --- 核心修改:图例位置 --- + # bbox_to_anchor=(1.1, 0.2) 的意思是: + # 锚点位于坐标轴右侧(1.1倍宽位置),垂直方向在底部(0.2倍高位置) + # loc='upper left' 意思是图例的左上角对齐这个锚点 + legend = plt.legend( + loc='upper left', + bbox_to_anchor=(1.1, 0.3), # 调整这里的 0.3 可以上下移动图例 + title="Models", + title_fontsize=16, + fontsize=12, + frameon=False, # 无边框 + labelspacing=0.8 # 图例行间距 + ) + + # 设置图例标题对齐方式 (左对齐) + legend._legend_box.align = "left" + + plt.title('Multi-Model Performance Comparison', size=18, y=1.08, color='#333333') + + if output_file: + plt.savefig(output_file, dpi=300, bbox_inches='tight') # bbox_inches='tight' 会自动裁剪白边 + print(f"Plot saved to {output_file}") + else: + plt.show() + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('files', metavar='F', type=str, nargs='+', help='YAML files') + parser.add_argument('--out', type=str, default=None, help='Output file') + + # 调试用(如果你直接运行脚本,请取消注释并填入你的文件名) + # sys.argv = ['plot.py', 'aggregated_results.yaml', 'aggregated_results2.yaml', '--out', 'fixed_radar.png'] + + args = parser.parse_args() + data, metrics_labels = load_data(args.files) + plot_radar(data, metrics_labels, output_file=args.out) diff --git a/robogauge/utils/task_register.py b/robogauge/utils/task_register.py index df7df5b..2cc5f40 100644 --- a/robogauge/utils/task_register.py +++ b/robogauge/utils/task_register.py @@ -63,6 +63,8 @@ class TaskRegister(): sim_cfg.viewer.headless = args.headless if args.save_video is not None: sim_cfg.render.save_video = args.save_video + if args.write_tensorboard is not None: + gauger_cfg.write_tensorboard = args.write_tensorboard if hasattr(args, 'friction') and args.friction is not None: sim_cfg.domain_rand.friction = args.friction if hasattr(args, 'base_mass') and args.base_mass is not None: