Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions conf/offpolicy/task/sac/g1_sac/motrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ algo:
algo_params:
alpha_init: 0.001
target_entropy_ratio: 0.0
env:
domain_rand:
randomize_kp: false
randomize_kd: false
reward:
scales:
tracking_lin_vel: 2.2
Expand Down
3 changes: 3 additions & 0 deletions conf/ppo/task/g1_joystick/motrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ algo:
entropy_coef: 5.0e-3
env:
iterations: 3
domain_rand:
randomize_kp: false
randomize_kd: false
control_config:
action_scale: 0.5
commands:
Expand Down
6 changes: 6 additions & 0 deletions src/unilab/base/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ def get_physics_state(self) -> np.ndarray:
f"{self.__class__.__name__} does not support physics-state playback"
)

def get_actuator_gains(self) -> tuple[np.ndarray, np.ndarray]:
"""Return per-joint (kp, kd) arrays from the backend model."""
raise NotImplementedError(
f"{self.__class__.__name__} does not support reading actuator gains"
)

# ------------------------------------------------------------------ #
# Base kinematics #
# ------------------------------------------------------------------ #
Expand Down
6 changes: 6 additions & 0 deletions src/unilab/base/backend/mujoco_backend.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mujoco_uni.batch_env
pool.get_field(env_id, name) -> np.ndarray
提供了批量获取DR参数属性的功能,替换成这个接口

Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ def _translate_reset_randomization(

return translated or None

def get_actuator_gains(self) -> tuple[np.ndarray, np.ndarray]:
"""Return per-joint (kp, kd) arrays read from the current model state."""
kp = np.asarray(self._model.actuator_gainprm[:, 0], dtype=np.float64).copy()
kd = np.asarray(-self._model.actuator_biasprm[:, 2], dtype=np.float64).copy()
return kp, kd

def _apply_position_actuator_gains_to_model(
self,
model,
Expand Down
50 changes: 36 additions & 14 deletions src/unilab/dr/dr_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ def zero_actions(num_reset: int, num_action: int) -> np.ndarray:
return np.zeros((num_reset, num_action), dtype=get_global_dtype())


def build_common_reset_randomization(env: Any, num_reset: int) -> ResetRandomizationPayload | None:
def build_common_reset_randomization(
env: Any,
num_reset: int,
*,
base_kp: np.ndarray | None = None,
base_kd: np.ndarray | None = None,
) -> ResetRandomizationPayload | None:
domain_rand = getattr(env.cfg, "domain_rand", None)
if domain_rand is None:
return None
Expand All @@ -33,25 +39,41 @@ def build_common_reset_randomization(env: Any, num_reset: int) -> ResetRandomiza
payload.base_com_offset = base_com_offset

num_actuators = getattr(env, "_num_action", None)
if num_actuators is not None and getattr(domain_rand, "randomize_kp", False):
low, high = domain_rand.kp_multiplier_range
kp_multiplier = np.random.uniform(low, high, size=(num_reset, 1))
base_kp = float(env.cfg.control_config.Kp)
payload.kp = np.broadcast_to(base_kp * kp_multiplier, (num_reset, num_actuators)).copy()

if num_actuators is not None and getattr(domain_rand, "randomize_kd", False):
low, high = domain_rand.kd_multiplier_range
kd_multiplier = np.random.uniform(low, high, size=(num_reset, 1))
base_kd = float(env.cfg.control_config.Kd)
payload.kd = np.broadcast_to(base_kd * kd_multiplier, (num_reset, num_actuators)).copy()
need_kp = num_actuators is not None and getattr(domain_rand, "randomize_kp", False)
need_kd = num_actuators is not None and getattr(domain_rand, "randomize_kd", False)

if need_kp or need_kd:
assert num_actuators is not None

if need_kp:
kp = (
base_kp
if base_kp is not None
else np.full(num_actuators, float(env.cfg.control_config.Kp))
)
low, high = domain_rand.kp_multiplier_range
payload.kp = (kp * np.random.uniform(low, high, (num_reset, 1))).astype(np.float64)

if need_kd:
kd = (
base_kd
if base_kd is not None
else np.full(num_actuators, float(env.cfg.control_config.Kd))
)
low, high = domain_rand.kd_multiplier_range
payload.kd = (kd * np.random.uniform(low, high, (num_reset, 1))).astype(np.float64)

return None if payload.is_empty() else payload


def validate_common_reset_randomization(
env: Any, capabilities: DomainRandomizationCapabilities
env: Any,
capabilities: DomainRandomizationCapabilities,
*,
base_kp: np.ndarray | None = None,
base_kd: np.ndarray | None = None,
) -> frozenset[str]:
payload = build_common_reset_randomization(env, num_reset=1)
payload = build_common_reset_randomization(env, num_reset=1, base_kp=base_kp, base_kd=base_kd)
if payload is None:
return frozenset()
return capabilities.get_unsupported_reset_terms(payload.requested_terms())
Expand Down
17 changes: 15 additions & 2 deletions src/unilab/envs/locomotion/common/dr_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,18 @@ class LocomotionDRProvider(DomainRandomizationProvider):

# ── shared methods ───────────────────────────────────────────────

def _get_base_actuator_gains(self, env: Any) -> tuple[np.ndarray | None, np.ndarray | None]:
"""Return (base_kp, base_kd) for per-joint kp/kd domain randomization.

Override to provide per-joint gains cached at init time.
Returns ``(None, None)`` by default, which falls back to scalar
``ControlConfig.Kp`` / ``ControlConfig.Kd``.
"""
return None, None

def validate(self, env: Any, capabilities: DomainRandomizationCapabilities) -> None:
validate_common_reset_randomization(env, capabilities)
base_kp, base_kd = self._get_base_actuator_gains(env)
validate_common_reset_randomization(env, capabilities, base_kp=base_kp, base_kd=base_kd)
validate_interval_push_support(env, capabilities)

def build_interval_randomization_plan(
Expand Down Expand Up @@ -87,12 +97,15 @@ def build_reset_plan(self, env: Any, env_ids: np.ndarray) -> ResetPlan:
"last_actions": zero_actions(num_reset, env._num_action),
}
info_updates.update(self._build_extra_info_updates(env, num_reset))
base_kp, base_kd = self._get_base_actuator_gains(env)
return ResetPlan(
env_ids=env_ids,
qpos=qpos,
qvel=qvel,
info_updates=info_updates,
randomization=build_common_reset_randomization(env, num_reset),
randomization=build_common_reset_randomization(
env, num_reset, base_kp=base_kp, base_kd=base_kd
),
)

# ── template: build_reset_observation ────────────────────────────
Expand Down
25 changes: 23 additions & 2 deletions src/unilab/envs/locomotion/g1/joystick.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
from unilab.envs.locomotion.g1.base import G1BaseCfg, G1BaseEnv


@dataclass
class G1DomainRandConfig(DomainRandConfig):
randomize_kp: bool = True
kp_multiplier_range: list[float] = field(default_factory=lambda: [0.9, 1.1])

randomize_kd: bool = True
kd_multiplier_range: list[float] = field(default_factory=lambda: [0.9, 1.1])


@dataclass
class InitState:
pos = [0.0, 0.0, 0.754]
Expand Down Expand Up @@ -107,12 +116,19 @@ class G1JoystickPPOCfg(G1BaseCfg):
init_state: InitState = field(default_factory=InitState)
commands: Commands = field(default_factory=Commands)
reward_config: RewardConfigPPO | None = None
domain_rand: DomainRandConfig = field(default_factory=DomainRandConfig)
domain_rand: G1DomainRandConfig = field(default_factory=G1DomainRandConfig)
gait_phase_init_mode: str = "offset_phase"
reset_base_qvel_limit: float = 0.5


class G1JoystickDomainRandomizationProvider(LocomotionDRProvider):
def __init__(self, *, base_kp: np.ndarray | None = None, base_kd: np.ndarray | None = None):
self._base_kp = base_kp
self._base_kd = base_kd

def _get_base_actuator_gains(self, env: Any) -> tuple[np.ndarray | None, np.ndarray | None]:
return self._base_kp, self._base_kd

def _get_qvel_limit(self, env: Any) -> float:
return float(env.cfg.reset_base_qvel_limit)

Expand Down Expand Up @@ -173,7 +189,12 @@ def __init__(self, cfg: G1JoystickPPOCfg, num_envs=1, backend_type="mujoco"):
self._upper_body_pose_weights = build_upper_body_pose_weights(self._reward_cfg.pose_weights)

self._init_reward_functions()
self._init_domain_randomization(G1JoystickDomainRandomizationProvider())
if cfg.domain_rand.randomize_kp or cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity and to avoid relying on a local variable after initialization, consider calling self._backend.get_actuator_gains() instead of backend.get_actuator_gains() here. It makes it explicit that the gains are being read from the env’s active backend instance.

Suggested change
base_kp, base_kd = backend.get_actuator_gains()
base_kp, base_kd = self._backend.get_actuator_gains()

Copilot uses AI. Check for mistakes.
dr_provider = G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
else:
dr_provider = G1JoystickDomainRandomizationProvider()
self._init_domain_randomization(dr_provider)

Comment on lines +192 to 198

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain-randomization provider selection logic is duplicated here and in g1/joystick_sac.py. To reduce the chance of future drift (e.g., adding a new DR term in one env but not the other), consider factoring this into a shared helper (or a small method on the base env) that returns the correctly-configured provider.

Suggested change
if cfg.domain_rand.randomize_kp or cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()
dr_provider = G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
else:
dr_provider = G1JoystickDomainRandomizationProvider()
self._init_domain_randomization(dr_provider)
dr_provider = self._create_domain_randomization_provider(backend)
self._init_domain_randomization(dr_provider)
def _create_domain_randomization_provider(
self, backend: Any
) -> G1JoystickDomainRandomizationProvider:
if self._cfg.domain_rand.randomize_kp or self._cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()
return G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
return G1JoystickDomainRandomizationProvider()

Copilot uses AI. Check for mistakes.
@property
def obs_groups_spec(self) -> dict[str, int]:
Expand Down
11 changes: 8 additions & 3 deletions src/unilab/envs/locomotion/g1/joystick_sac.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from unilab.base.dtype_config import get_global_dtype
from unilab.envs.locomotion.common import rewards
from unilab.envs.locomotion.common.commands import Commands
from unilab.envs.locomotion.common.domain_rand import DomainRandConfig
from unilab.envs.locomotion.common.rewards import RewardContext
from unilab.envs.locomotion.g1.base import G1BaseCfg, G1BaseEnv
from unilab.envs.locomotion.g1.joystick import (
G1DomainRandConfig,
G1JoystickDomainRandomizationProvider,
G1JoystickPPO,
InitState,
Expand Down Expand Up @@ -55,7 +55,7 @@ class G1JoystickSACCfg(G1BaseCfg):
init_state: InitState = field(default_factory=InitState)
commands: Commands = field(default_factory=Commands)
control_config: ControlConfigSAC = field(default_factory=ControlConfigSAC) # type: ignore[assignment]
domain_rand: DomainRandConfig = field(default_factory=DomainRandConfig)
domain_rand: G1DomainRandConfig = field(default_factory=G1DomainRandConfig)
gait_phase_init_mode: str = "offset_phase"
reset_base_qvel_limit: float = 0.5

Expand Down Expand Up @@ -96,7 +96,12 @@ def __init__(self, cfg: G1JoystickSACCfg, num_envs=1, backend_type="mujoco"):
)

self._init_reward_functions()
self._init_domain_randomization(G1JoystickDomainRandomizationProvider())
if cfg.domain_rand.randomize_kp or cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability, prefer self._backend.get_actuator_gains() over the local backend variable here, so it’s clear the gains are read from the env’s configured backend instance.

Suggested change
base_kp, base_kd = backend.get_actuator_gains()
base_kp, base_kd = self._backend.get_actuator_gains()

Copilot uses AI. Check for mistakes.
dr_provider = G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
else:
dr_provider = G1JoystickDomainRandomizationProvider()
self._init_domain_randomization(dr_provider)

Comment on lines +99 to 105

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain-randomization provider selection logic is duplicated with g1/joystick.py. Consider extracting a shared helper so PPO/SAC variants stay in sync when DR behavior changes (e.g., additional reset terms or different gain-caching rules).

Suggested change
if cfg.domain_rand.randomize_kp or cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()
dr_provider = G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
else:
dr_provider = G1JoystickDomainRandomizationProvider()
self._init_domain_randomization(dr_provider)
dr_provider = self._create_domain_randomization_provider(cfg, backend)
self._init_domain_randomization(dr_provider)
def _create_domain_randomization_provider(self, cfg: G1JoystickSACCfg, backend):
"""Create the DR provider while staying aligned with PPO/shared logic when available."""
parent_factory = getattr(super(), "_create_domain_randomization_provider", None)
if callable(parent_factory):
return parent_factory(cfg, backend)
if cfg.domain_rand.randomize_kp or cfg.domain_rand.randomize_kd:
base_kp, base_kd = backend.get_actuator_gains()
return G1JoystickDomainRandomizationProvider(base_kp=base_kp, base_kd=base_kd)
return G1JoystickDomainRandomizationProvider()

Copilot uses AI. Check for mistakes.
def _compute_obs(
self, info: dict, linvel, gyro, gravity, dof_pos, dof_vel
Expand Down