Source code for unilab.envs.locomotion.common.base
from __future__ import annotations
from dataclasses import dataclass, field
from typing import ClassVar
import gymnasium as gym
import numpy as np
from unilab.base.backend import SimBackend
from unilab.base.base import EnvCfg
from unilab.base.np_env import NpEnv, NpEnvState
from unilab.dtype_config import get_global_dtype
from unilab.envs.locomotion.common.terrain_spawn import BaseSpawnManager
[docs]
@dataclass
class Sensor:
local_linvel = "local_linvel"
gyro = "gyro"
[docs]
@dataclass
class ControlConfigBase:
action_scale: float = 0.25
simulate_action_latency: bool = False
[docs]
@dataclass
class PdControlConfig(ControlConfigBase):
"""``ControlConfigBase`` plus shared PD-actuator gains (Go1/Go2/Go2W defaults)."""
Kp: float = 35.0
Kd: float = 0.5
[docs]
@dataclass
class BaseNoiseConfig:
level: float = 0.0
scale_joint_angle: float = 0.03
scale_joint_vel: float = 0.5
scale_gyro: float = 0.2
scale_gravity: float = 0.05
scale_linvel: float = 0.1
[docs]
@dataclass
class LocomotionBaseCfg(EnvCfg):
control_config: ControlConfigBase = field(default_factory=ControlConfigBase)
noise_config: BaseNoiseConfig = field(default_factory=BaseNoiseConfig)
sensor: Sensor = field(default_factory=Sensor)
sim_dt: float = 0.01
ctrl_dt: float = 0.02
[docs]
class LocomotionBaseEnv(NpEnv):
"""Common base environment for locomotion tasks (G1, Go1, Go2, etc.)."""
_cfg: LocomotionBaseCfg
_keyframe_name: ClassVar[str] = "home"
_use_global_dtype: ClassVar[bool] = True
[docs]
def __init__(self, cfg: LocomotionBaseCfg, backend: SimBackend, num_envs: int = 1):
super().__init__(cfg, backend, num_envs)
self._init_action_space()
self._num_action = self._action_space.shape[0]
self._init_buffers()
self._spawn: BaseSpawnManager = BaseSpawnManager()
def _init_action_space(self) -> None:
ctrl_range = self._backend.get_actuator_ctrl_range()
nu = self._backend.num_actuators
self._action_space = gym.spaces.Box(ctrl_range[:, 0], ctrl_range[:, 1], (nu,), dtype=float) # type: ignore[assignment, arg-type]
@property
def action_space(self) -> gym.spaces.Box:
return self._action_space # type: ignore[no-any-return]
def _init_buffers(self) -> None:
dtype = get_global_dtype() if self._use_global_dtype else np.float32
raw_qpos = self._backend.get_keyframe_qpos(self._keyframe_name)
self._init_qpos = (
np.asarray(raw_qpos, dtype=dtype) if self._use_global_dtype else np.asarray(raw_qpos)
)
self.default_angles = np.asarray(self._init_qpos[-self._num_action :], dtype=dtype)
raw_qvel = self._backend.get_init_qvel()
self._init_qvel = (
np.asarray(raw_qvel, dtype=dtype) if self._use_global_dtype else np.asarray(raw_qvel)
)
[docs]
def apply_action(self, actions: np.ndarray, state: NpEnvState) -> np.ndarray:
state.info["last_actions"] = state.info.get("current_actions", np.zeros_like(actions))
state.info["current_actions"] = actions
exec_actions = (
state.info["last_actions"]
if self._cfg.control_config.simulate_action_latency
else actions
)
ctrl: np.ndarray = (
exec_actions * self._cfg.control_config.action_scale + self.default_angles
)
return ctrl
def _obs_noise(self, data: np.ndarray, scale: float) -> np.ndarray:
"""Apply per-step uniform observation noise scaled by ``noise_config.level``."""
level = float(self._cfg.noise_config.level)
if level <= 0.0:
return data
noise = np.random.uniform(-1.0, 1.0, data.shape).astype(data.dtype) * level * scale
return data + noise
[docs]
def get_local_linvel(self) -> np.ndarray:
local_linvel: np.ndarray = self._backend.get_sensor_data(self._cfg.sensor.local_linvel)
return local_linvel
[docs]
def get_gyro(self) -> np.ndarray:
gyro: np.ndarray = self._backend.get_sensor_data(self._cfg.sensor.gyro)
return gyro
[docs]
def get_dof_pos(self) -> np.ndarray:
dof_pos: np.ndarray = self._backend.get_dof_pos()
return dof_pos
[docs]
def get_dof_vel(self) -> np.ndarray:
dof_vel: np.ndarray = self._backend.get_dof_vel()
return dof_vel