v0.1.2
This commit is contained in:
7
robogauge/tasks/__init__.py
Normal file
7
robogauge/tasks/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from robogauge.utils.task_register import task_register
|
||||
from robogauge.tasks.simulator.mujoco_config import MujocoConfig
|
||||
from robogauge.tasks.robots import RobotConfig
|
||||
from robogauge.tasks.pipeline import BasePipeline
|
||||
from robogauge.tasks.gauge import BaseGaugeConfig
|
||||
|
||||
task_register.register('base', BasePipeline, MujocoConfig, BaseGaugeConfig, RobotConfig)
|
||||
4
robogauge/tasks/gauge/__init__.py
Normal file
4
robogauge/tasks/gauge/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .base_gauge import BaseGauge
|
||||
from .base_gauge_config import BaseGaugeConfig
|
||||
from .flat.flat_gauge import FlatGauge
|
||||
from .flat.flat_gauge_config import FlatGaugeConfig
|
||||
29
robogauge/tasks/gauge/base_gauge.py
Normal file
29
robogauge/tasks/gauge/base_gauge.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : base_gauge.py
|
||||
@Time : 2025/11/27 15:55:19
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Base Gauge for Robogauge
|
||||
'''
|
||||
from robogauge.tasks.robots.base_robot_config import RobotConfig
|
||||
from robogauge.tasks.gauge.base_gauge_config import BaseGaugeConfig
|
||||
|
||||
class BaseGauge:
|
||||
def __init__(self, cfg: BaseGaugeConfig):
|
||||
self.cfg = cfg
|
||||
|
||||
def is_reset(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_done(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_goal(self) -> dict:
|
||||
goal = {}
|
||||
return goal
|
||||
|
||||
def update_metrics(self, sim_info: dict):
|
||||
...
|
||||
|
||||
26
robogauge/tasks/gauge/base_gauge_config.py
Normal file
26
robogauge/tasks/gauge/base_gauge_config.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : base_gauge_config.py
|
||||
@Time : 2025/11/27 15:55:11
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Base Gauge Configuration
|
||||
'''
|
||||
from robogauge.utils.config import Config
|
||||
|
||||
class BaseGaugeConfig(Config):
|
||||
gauge_class = 'BaseGauge'
|
||||
|
||||
class assets:
|
||||
terrain_xml = '{ROBOGAUGE_ROOT_DIR}/resources/terrains/flat.xml'
|
||||
terrain_spawn_xy = [0, 0] # x y [m]
|
||||
|
||||
class metrics:
|
||||
dof_limits = True
|
||||
|
||||
class commands:
|
||||
stance = True
|
||||
max_lin_vel = True
|
||||
diagonal_lin_vel = True
|
||||
|
||||
13
robogauge/tasks/gauge/flat/flat_gauge.py
Normal file
13
robogauge/tasks/gauge/flat/flat_gauge.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : flat_gauge.py
|
||||
@Time : 2025/11/27 16:03:11
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Flat Gauge Implementation
|
||||
'''
|
||||
from robogauge.tasks.gauge.base_gauge import BaseGauge
|
||||
|
||||
class FlatGauge(BaseGauge):
|
||||
...
|
||||
13
robogauge/tasks/gauge/flat/flat_gauge_config.py
Normal file
13
robogauge/tasks/gauge/flat/flat_gauge_config.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : flat_gauge_config.py
|
||||
@Time : 2025/11/27 16:03:02
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Flat Gauge Configuration
|
||||
'''
|
||||
from robogauge.tasks.gauge.base_gauge_config import BaseGaugeConfig
|
||||
|
||||
class FlatGaugeConfig(BaseGaugeConfig):
|
||||
...
|
||||
1
robogauge/tasks/pipeline/__init__.py
Normal file
1
robogauge/tasks/pipeline/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .base_pipeline import BasePipeline
|
||||
58
robogauge/tasks/pipeline/base_pipeline.py
Normal file
58
robogauge/tasks/pipeline/base_pipeline.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : base_pipeline.py
|
||||
@Time : 2025/11/27 15:53:26
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Base Pipeline for Robogauge
|
||||
'''
|
||||
import traceback
|
||||
from robogauge.utils.logger import logger
|
||||
from robogauge.tasks.simulator import MujocoSimulator, MujocoConfig
|
||||
from robogauge.tasks.robots import BaseRobot, RobotConfig
|
||||
from robogauge.tasks.gauge import BaseGauge, BaseGaugeConfig
|
||||
|
||||
class BasePipeline:
|
||||
def __init__(self,
|
||||
simulator_cfg: MujocoConfig,
|
||||
robot_cfg: RobotConfig,
|
||||
gauge_cfg: BaseGaugeConfig
|
||||
):
|
||||
self.simulator_cfg = simulator_cfg
|
||||
self.robot_cfg = robot_cfg
|
||||
self.gauge_cfg = gauge_cfg
|
||||
|
||||
self.sim: MujocoSimulator = eval(simulator_cfg.simulator_class)(simulator_cfg)
|
||||
self.robot: BaseRobot = eval(robot_cfg.robot_class)(robot_cfg)
|
||||
self.gauge: BaseGauge = eval(gauge_cfg.gauge_class)(gauge_cfg)
|
||||
|
||||
def load(self):
|
||||
self.sim.load(
|
||||
self.gauge_cfg.assets.terrain_xml,
|
||||
self.robot_cfg.assets.robot_xml,
|
||||
self.gauge_cfg.assets.terrain_spawn_xy,
|
||||
self.robot_cfg.assets.robot_spawn_height
|
||||
)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.load()
|
||||
info = self.sim.step()
|
||||
frame_skip = int(self.robot_cfg.control.control_dt / self.simulator_cfg.physics.simulation_dt)
|
||||
logger.info(f"Sim FPS: {1.0 / self.simulator_cfg.physics.simulation_dt:.2f}, Control FPS: {1.0 / self.robot_cfg.control.control_dt:.2f}, Frame Skip: {frame_skip:d}")
|
||||
logger.info("Starting pipeline...")
|
||||
while not self.gauge.is_done():
|
||||
goal = self.gauge.get_goal()
|
||||
obs = self.robot.build_observation(info, goal)
|
||||
action = self.robot.get_action(obs)
|
||||
for _ in range(frame_skip):
|
||||
self.sim.apply_action(action)
|
||||
info = self.sim.step()
|
||||
self.gauge.update_metrics(info)
|
||||
if self.gauge.is_reset():
|
||||
self.sim.reset()
|
||||
info = self.sim.step()
|
||||
finally:
|
||||
self.sim.close_viewer()
|
||||
logger.info("Pipeline execution finished.")
|
||||
4
robogauge/tasks/robots/__init__.py
Normal file
4
robogauge/tasks/robots/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .base_robot_config import RobotConfig
|
||||
from .base_robot import BaseRobot
|
||||
# from .go2.go2_config import Go2Config
|
||||
# from .go2.go2_controller import Go2Controller
|
||||
29
robogauge/tasks/robots/base_robot.py
Normal file
29
robogauge/tasks/robots/base_robot.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : base_robot.py
|
||||
@Time : 2025/11/27 15:53:57
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Base Robot Class
|
||||
'''
|
||||
import torch
|
||||
import numpy as np
|
||||
from robogauge.tasks.robots.base_robot_config import RobotConfig
|
||||
|
||||
class BaseRobot:
|
||||
def __init__(self, cfg: RobotConfig):
|
||||
self.num_act = cfg.mdp.num_actions
|
||||
self.num_obs = cfg.mdp.num_observations
|
||||
self.model = None
|
||||
|
||||
def load_model(self):
|
||||
...
|
||||
|
||||
def build_observation(self, sim_info: dict, goal_info: dict) -> np.ndarray:
|
||||
obs = np.zeros(self.num_obs)
|
||||
return obs
|
||||
|
||||
def get_action(self, obs) -> np.ndarray:
|
||||
action = np.zeros_like(self.num_act)
|
||||
return action
|
||||
34
robogauge/tasks/robots/base_robot_config.py
Normal file
34
robogauge/tasks/robots/base_robot_config.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : base_robot_config.py
|
||||
@Time : 2025/11/27 15:53:47
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Base Robot Configuration
|
||||
'''
|
||||
from robogauge.utils.config import Config
|
||||
|
||||
class RobotConfig(Config):
|
||||
robot_class = 'BaseRobot'
|
||||
|
||||
class assets:
|
||||
robot_xml = "{ROBOGAUGE_ROOT_DIR}/resources/robots/go2/go2.xml"
|
||||
robot_spawn_height = 0.1 # z [m]
|
||||
|
||||
class control:
|
||||
torch_script_model_path = "{ROBOGAUGE_ROOT_DIR}/resources/models/go2/go2_cts_61500.pt"
|
||||
control_dt = 0.02 # 50 Hz
|
||||
action_scale = 0.25 # target pos = action_scale * action * default_pos
|
||||
stiffness = 20.0 # [N*m/rad]
|
||||
damping = 0.5 # [N*m*s/rad]
|
||||
|
||||
class mdp:
|
||||
num_observations = 46
|
||||
num_actions = 12
|
||||
|
||||
class commands:
|
||||
lin_vel_x = [-1, 1] # min max [m/s]
|
||||
lin_vel_y = [-1, 1] # min max [m/s]
|
||||
ang_vel_yaw = [-1, 1] # min max [rad/s]
|
||||
|
||||
0
robogauge/tasks/robots/go2/go2.py
Normal file
0
robogauge/tasks/robots/go2/go2.py
Normal file
20
robogauge/tasks/robots/go2/go2_config.py
Normal file
20
robogauge/tasks/robots/go2/go2_config.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : go2_config.py
|
||||
@Time : 2025/11/27 16:03:27
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Go2 Robot Configuration
|
||||
'''
|
||||
from robogauge.tasks.robots import RobotConfig
|
||||
|
||||
class Go2Config(RobotConfig):
|
||||
class assets:
|
||||
robot_xml = "{ROBOGAUGE_ROOT_DIR}/resources/robots/go2/go2.xml"
|
||||
robot_spawn_height = 0.1 # z [m]
|
||||
|
||||
class control:
|
||||
control_dt = 0.02 # 50 Hz
|
||||
action_scale = 0.25 # scale for normalized actions
|
||||
|
||||
2
robogauge/tasks/simulator/__init__.py
Normal file
2
robogauge/tasks/simulator/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .mujoco_simulator import MujocoSimulator
|
||||
from .mujoco_config import MujocoConfig
|
||||
25
robogauge/tasks/simulator/mujoco_config.py
Normal file
25
robogauge/tasks/simulator/mujoco_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : mujoco_config.py
|
||||
@Time : 2025/11/27 15:55:34
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : Mujoco Simulator Configuration
|
||||
'''
|
||||
from robogauge.utils.config import Config
|
||||
|
||||
class MujocoConfig(Config):
|
||||
simulator_class = 'MujocoSimulator'
|
||||
|
||||
class physics:
|
||||
simulation_dt = 0.005 # 200 Hz
|
||||
|
||||
class viewer:
|
||||
headless = False
|
||||
block_rendering = True # Whether to block rendering in the viewer loop.
|
||||
|
||||
class render:
|
||||
save_video = False
|
||||
height = 480
|
||||
width = 640
|
||||
143
robogauge/tasks/simulator/mujoco_simulator.py
Normal file
143
robogauge/tasks/simulator/mujoco_simulator.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
@File : mujoco_simulator.py
|
||||
@Time : 2025/11/27 15:54:20
|
||||
@Author : wty-yy
|
||||
@Version : 1.0
|
||||
@Blog : https://wty-yy.github.io/
|
||||
@Desc : None
|
||||
'''
|
||||
import mujoco
|
||||
import mujoco.viewer
|
||||
from dm_control import mjcf
|
||||
|
||||
import time
|
||||
import imageio
|
||||
import numpy as np
|
||||
|
||||
from robogauge.utils.logger import logger
|
||||
from robogauge.utils.helpers import pares_path
|
||||
from robogauge.tasks.simulator.mujoco_config import MujocoConfig
|
||||
|
||||
class MujocoSimulator:
|
||||
def __init__(self, sim_cfg: MujocoConfig):
|
||||
self.cfg = sim_cfg
|
||||
self.terrain_xml = None
|
||||
self.robot_xml = None
|
||||
self.terrain_spawn_xy = None
|
||||
self.robot_spawn_height = None
|
||||
self.viewer = None
|
||||
self.renderer = None
|
||||
self.vid_writer = None
|
||||
self.vid_count = 0
|
||||
self._pause = True
|
||||
|
||||
def load(
|
||||
self,
|
||||
terrain_xml: str = None,
|
||||
robot_xml: str = None,
|
||||
terrain_spawn_xy: list = None,
|
||||
robot_spawn_height: float = None,
|
||||
):
|
||||
""" Load terrain and robot into the simulator, support re-loading. """
|
||||
if terrain_xml is not None:
|
||||
self.terrain_xml = pares_path(terrain_xml)
|
||||
if robot_xml is not None:
|
||||
self.robot_xml = pares_path(robot_xml)
|
||||
if terrain_spawn_xy is not None:
|
||||
self.terrain_spawn_xy = terrain_spawn_xy
|
||||
if robot_spawn_height is not None:
|
||||
self.robot_spawn_height = robot_spawn_height
|
||||
|
||||
terrain_xml = self.terrain_xml
|
||||
robot_xml = self.robot_xml
|
||||
terrain_spawn_xy = self.terrain_spawn_xy
|
||||
robot_spawn_height = self.robot_spawn_height
|
||||
if terrain_xml is None or robot_xml is None:
|
||||
raise ValueError("Terrain and robot XML paths must be provided.")
|
||||
|
||||
robot_mjcf = mjcf.from_path(robot_xml)
|
||||
terrain_mjcf = mjcf.from_path(terrain_xml)
|
||||
for j in robot_mjcf.find_all('joint'):
|
||||
if j.tag == 'freejoint':
|
||||
j.remove()
|
||||
attachment_frame = terrain_mjcf.attach(robot_mjcf)
|
||||
attachment_frame.add('freejoint')
|
||||
attachment_frame.pos = [*terrain_spawn_xy, robot_spawn_height]
|
||||
|
||||
if self.viewer is not None:
|
||||
self.close_viewer()
|
||||
self.mj_physics = mjcf.Physics.from_mjcf_model(terrain_mjcf)
|
||||
self.mj_model = self.mj_physics.model.ptr
|
||||
self.mj_data = self.mj_physics.data.ptr
|
||||
self.mj_model.opt.timestep = self.cfg.physics.simulation_dt
|
||||
self.headless = self.cfg.viewer.headless
|
||||
if self.cfg.render.save_video and self.headless:
|
||||
logger.warning("Cannot save video in headless mode, disabling video saving.")
|
||||
self.cfg.render.save_video = False
|
||||
if not self.headless:
|
||||
self.viewer = mujoco.viewer.launch_passive(self.mj_model, self.mj_data, key_callback=self.key_callback)
|
||||
self.last_render_time = time.time()
|
||||
if self.cfg.render.save_video:
|
||||
self.renderer = mujoco.Renderer(self.mj_model, height=self.cfg.render.height, width=self.cfg.render.width)
|
||||
|
||||
vid_dir = logger.log_dir / "videos"
|
||||
vid_dir.mkdir(parents=True, exist_ok=True)
|
||||
vid_path = str(vid_dir / f"sim_video_{self.vid_count:03d}.mp4")
|
||||
self.vid_writer = imageio.get_writer(
|
||||
vid_path,
|
||||
fps=int(1 / self.cfg.physics.simulation_dt),
|
||||
)
|
||||
logger.info(f"Saving simulation video to: {vid_path}")
|
||||
self.vid_count += 1
|
||||
self._pause = False
|
||||
|
||||
def key_callback(self, keycode):
|
||||
if keycode == 32:
|
||||
self._pause = not self._pause
|
||||
logger.info(f"Pause toggled: {self._pause}")
|
||||
|
||||
def step(self) -> dict:
|
||||
""" Simulation step, pause will block thread. """
|
||||
while self._pause:
|
||||
time.sleep(0.1)
|
||||
self.mj_physics.step()
|
||||
if self.viewer is not None:
|
||||
if self.viewer.is_running():
|
||||
time_untile_next_render = self.cfg.physics.simulation_dt - (time.time() - self.last_render_time)
|
||||
if time_untile_next_render > 0:
|
||||
time.sleep(time_untile_next_render)
|
||||
self.viewer.sync()
|
||||
if self.vid_writer is not None:
|
||||
self.renderer.update_scene(self.mj_data, camera=self.viewer.cam)
|
||||
frame = self.renderer.render()
|
||||
self.vid_writer.append_data(frame)
|
||||
self.last_render_time = time.time()
|
||||
else:
|
||||
logger.warning("Viewer closed by user, stop video recording.")
|
||||
self.close_viewer()
|
||||
|
||||
info = {}
|
||||
return info
|
||||
|
||||
def reset(self):
|
||||
""" Reset the simulator to initial state. """
|
||||
self.mj_physics.reset()
|
||||
if self.viewer is not None:
|
||||
self.viewer.sync()
|
||||
|
||||
def apply_action(self, action: np.ndarray):
|
||||
""" Apply action to the simulator. """
|
||||
self.mj_data.ctrl[:] = action
|
||||
|
||||
def close_viewer(self):
|
||||
""" Close the viewer and video writer. """
|
||||
if self.viewer is not None:
|
||||
self.viewer.close()
|
||||
self.viewer = None
|
||||
logger.info("Closing viewer.")
|
||||
if self.vid_writer is not None:
|
||||
self.vid_writer.close()
|
||||
self.vid_writer = None
|
||||
self.renderer = None
|
||||
logger.info("Closing video writer.")
|
||||
Reference in New Issue
Block a user