Skip to content

Commit 8758945

Browse files
committed
refactor(a2arm): complete manager-based migration cleanup
1 parent 8a23d47 commit 8758945

32 files changed

Lines changed: 1388 additions & 603 deletions

conf/ppo_cse/task/a2arm_pos_force/base.yaml

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ env:
2121
history:
2222
_target_: unilab.tasks.locomotion.a2arm.observations.A2ArmActorHistoryCfg
2323
func: unilab.tasks.locomotion.a2arm.observations.A2ArmActorHistory
24-
params: {clip: 100.0, actor_noise: true}
24+
params:
25+
clip: 100.0
26+
actor_noise: true
27+
history_length: "${oc.select:algo.num_actor_history,32}"
2528
critic:
2629
terms:
2730
history:
2831
_target_: unilab.tasks.locomotion.a2arm.observations.A2ArmCriticHistoryCfg
2932
func: unilab.tasks.locomotion.a2arm.observations.A2ArmCriticHistory
30-
params: {clip: 100.0, actor_noise: false}
33+
params:
34+
clip: 100.0
35+
actor_noise: false
36+
history_length: "${oc.select:algo.num_critic_history,3}"
3137
actions:
3238
joint_pd:
3339
_target_: unilab.tasks.locomotion.a2arm.actions.A2ArmPdActionCfg
@@ -85,11 +91,11 @@ env:
8591
critic_observation_group: critic
8692

8793
reward:
88-
tracking_lin_vel_force_world: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 2.0, params: {name: tracking_lin_vel_force_world}}
89-
tracking_ee_force_world: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 2.0, params: {name: tracking_ee_force_world}}
90-
tracking_ang_vel: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: tracking_ang_vel}}
94+
tracking_lin_vel_force_world: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 2.0, params: {name: tracking_lin_vel_force_world, sigma: 0.25}}
95+
tracking_ee_force_world: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 2.0, params: {name: tracking_ee_force_world, sigma: 1.0}}
96+
tracking_ang_vel: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: tracking_ang_vel, sigma: 0.25}}
9197
orientation: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -1.5, params: {name: orientation}}
92-
ref_dof_leg: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 3.0, params: {name: ref_dof_leg}}
98+
ref_dof_leg: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 3.0, params: {name: ref_dof_leg, scale: 0.1}}
9399
alive: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.5, params: {name: alive}}
94100
lin_vel_z: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -1.5, params: {name: lin_vel_z}}
95101
ang_vel_xy: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -0.1, params: {name: ang_vel_xy}}
@@ -100,16 +106,16 @@ reward:
100106
dof_vel_arm: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -2.0e-4, params: {name: dof_vel_arm}}
101107
dof_acc: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -2.5e-7, params: {name: dof_acc}}
102108
dof_acc_arm: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -4.5e-7, params: {name: dof_acc_arm}}
103-
base_height: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -2.0, params: {name: base_height}}
109+
base_height: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -2.0, params: {name: base_height, target: 0.435}}
104110
hip_pos: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -0.5, params: {name: hip_pos}}
105-
torque_limits: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -0.005, params: {name: torque_limits}}
111+
torque_limits: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -0.005, params: {name: torque_limits, soft_limit: 0.9}}
106112
dof_pos_limits: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -10.0, params: {name: dof_pos_limits}}
107-
stand_still: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 0.5, params: {name: stand_still}}
113+
stand_still: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 0.5, params: {name: stand_still, scale: 0.05}}
108114
collision: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -5.0, params: {name: collision}}
109115
feet_contact_number: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 2.0, params: {name: feet_contact_number}}
110-
feet_air_time: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: feet_air_time}}
111-
feet_height: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: feet_height}}
112-
feet_height_high: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -15.0, params: {name: feet_height_high}}
116+
feet_air_time: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: feet_air_time, threshold: 0.5}}
117+
feet_height: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: 1.0, params: {name: feet_height, target: 0.12}}
118+
feet_height_high: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -15.0, params: {name: feet_height_high, target: 0.24}}
113119
feet_pos_xy: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -3.0, params: {name: feet_pos_xy}}
114120
feet_drag: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -8.0e-4, params: {name: feet_drag}}
115-
feet_contact_forces: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -1.0e-3, params: {name: feet_contact_forces}}
121+
feet_contact_forces: {func: unilab.tasks.locomotion.a2arm.rewards.a2arm_reward, weight: -1.0e-3, params: {name: feet_contact_forces, threshold: 200.0}}

scripts/benchmark/rl/benchmark_offpolicy_collector_active.py

Lines changed: 23 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@
3333
import sys
3434
import time
3535
from collections import defaultdict
36+
from collections.abc import Sequence
3637
from dataclasses import asdict, dataclass, field
3738
from datetime import datetime, timezone
3839
from importlib.util import find_spec
3940
from pathlib import Path
4041
from statistics import mean, median, pstdev
41-
from typing import Any, Sequence, cast
42+
from typing import Any, cast
4243

4344
import numpy as np
4445
import torch
@@ -1329,89 +1330,53 @@ def _format_dr_reset_timing_table(results: list[CollectorResult]) -> str:
13291330
)
13301331

13311332

1332-
def _format_set_state_detail_table(results: list[CollectorResult]) -> str:
1333-
"""Backend set_state sub-timing table (motrix keyset).
1334-
1335-
Renders the 14 motrix-oriented sub-keys next to the outer
1336-
``dr_reset_set_state_ms``. Backends that don't populate a key emit 0.0 so
1337-
columns stay stable across backends. MuJoCo runs will show 0.0 for the
1338-
motrix-only sub-keys; use :func:`_format_set_state_mujoco_table` for the
1339-
MuJoCo-oriented view instead.
1340-
"""
1333+
def _format_set_state_backend_table(
1334+
results: list[CollectorResult],
1335+
keys: Sequence[tuple[str, str]],
1336+
) -> str:
13411337
headers = (
13421338
"Algo",
13431339
"Task",
13441340
"Backend",
13451341
"Set state ms (%env, %active)",
1346-
*(label for _, label in _SET_STATE_MOTRIX_KEYS),
1342+
*(label for _, label in keys),
13471343
)
13481344
rows = []
13491345
for result in results:
1350-
env_step = result.phase_ms_per_vector_step.get("env_step_ms")
1351-
if env_step is None:
1346+
if result.phase_ms_per_vector_step.get("env_step_ms") is None:
13521347
continue
13531348
rows.append(
13541349
(
13551350
result.case.algo,
13561351
result.case.task,
13571352
result.case.runtime_sim_backend,
13581353
_format_np_env_timing(result, "dr_reset_set_state_ms"),
1359-
*(_format_set_state_sub_ms(result, key) for key, _ in _SET_STATE_MOTRIX_KEYS),
1354+
*(_format_set_state_sub_ms(result, key) for key, _ in keys),
13601355
)
13611356
)
13621357
return _format_table(headers, rows)
13631358

13641359

1360+
def _format_set_state_detail_table(results: list[CollectorResult]) -> str:
1361+
"""Backend set_state sub-timing table (motrix keyset).
1362+
1363+
Renders the 14 motrix-oriented sub-keys next to the outer
1364+
``dr_reset_set_state_ms``. Backends that don't populate a key emit 0.0 so
1365+
columns stay stable across backends. MuJoCo runs will show 0.0 for the
1366+
motrix-only sub-keys; use :func:`_format_set_state_mujoco_table` for the
1367+
MuJoCo-oriented view instead.
1368+
"""
1369+
return _format_set_state_backend_table(results, _SET_STATE_MOTRIX_KEYS)
1370+
1371+
13651372
def _format_set_state_mujoco_table(results: list[CollectorResult]) -> str:
13661373
"""Backend set_state sub-timing table (mujoco keyset)."""
1367-
headers = (
1368-
"Algo",
1369-
"Task",
1370-
"Backend",
1371-
"Set state ms (%env, %active)",
1372-
*(label for _, label in _SET_STATE_MUJOCO_KEYS),
1373-
)
1374-
rows = []
1375-
for result in results:
1376-
env_step = result.phase_ms_per_vector_step.get("env_step_ms")
1377-
if env_step is None:
1378-
continue
1379-
rows.append(
1380-
(
1381-
result.case.algo,
1382-
result.case.task,
1383-
result.case.runtime_sim_backend,
1384-
_format_np_env_timing(result, "dr_reset_set_state_ms"),
1385-
*(_format_set_state_sub_ms(result, key) for key, _ in _SET_STATE_MUJOCO_KEYS),
1386-
)
1387-
)
1388-
return _format_table(headers, rows)
1374+
return _format_set_state_backend_table(results, _SET_STATE_MUJOCO_KEYS)
13891375

13901376

13911377
def _format_set_state_mjwarp_table(results: list[CollectorResult]) -> str:
13921378
"""Backend set_state sub-timing table (mjwarp keyset)."""
1393-
headers = (
1394-
"Algo",
1395-
"Task",
1396-
"Backend",
1397-
"Set state ms (%env, %active)",
1398-
*(label for _, label in _SET_STATE_MJWARP_KEYS),
1399-
)
1400-
rows = []
1401-
for result in results:
1402-
env_step = result.phase_ms_per_vector_step.get("env_step_ms")
1403-
if env_step is None:
1404-
continue
1405-
rows.append(
1406-
(
1407-
result.case.algo,
1408-
result.case.task,
1409-
result.case.runtime_sim_backend,
1410-
_format_np_env_timing(result, "dr_reset_set_state_ms"),
1411-
*(_format_set_state_sub_ms(result, key) for key, _ in _SET_STATE_MJWARP_KEYS),
1412-
)
1413-
)
1414-
return _format_table(headers, rows)
1379+
return _format_set_state_backend_table(results, _SET_STATE_MJWARP_KEYS)
14151380

14161381

14171382
def _format_np_env_step_timing_table(results: list[CollectorResult]) -> str:

scripts/play_a2arm_pos_force_interactive.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@
2525
from unilab.base.backend.mujoco.playback import resolve_render_play_model_files
2626
from unilab.base.config_adapter import BackendAdapter, create_env
2727
from unilab.tasks.locomotion.a2arm.state import A2ArmPosForceState
28-
from unilab.training import ensure_registries, parse_checkpoint_path
28+
from unilab.training import algo_config_dict, ensure_registries, parse_checkpoint_path
2929
from unilab.visualization.a2arm_pos_force import (
30-
KEY_BACKSPACE,
31-
KEY_SPACE,
3230
clear_teleop_override,
3331
draw_markers,
3432
install_teleop_override,
@@ -49,13 +47,6 @@ def _select_device(cfg: DictConfig) -> str:
4947
return "cpu"
5048

5149

52-
def _algo_config_dict(cfg: DictConfig) -> dict[str, Any]:
53-
resolved = OmegaConf.to_container(cfg.algo, resolve=True)
54-
if not isinstance(resolved, dict):
55-
raise TypeError("cfg.algo must resolve to a mapping")
56-
return cast(dict[str, Any], resolved)
57-
58-
5950
def _backend_adapter(cfg: DictConfig) -> BackendAdapter:
6051
return BackendAdapter(
6152
cfg,
@@ -131,10 +122,9 @@ def play_interactive(cfg: DictConfig, device: str) -> None:
131122
sim_backend="mujoco",
132123
)
133124
wrapped = RslRlVecEnvWrapper(env, device=device)
134-
runner = CSEOnPolicyRunner(wrapped, _algo_config_dict(cfg), log_dir=None, device=device)
125+
runner = CSEOnPolicyRunner(wrapped, algo_config_dict(cfg), log_dir=None, device=device)
135126
runner.load(str(checkpoint))
136127
policy = runner.get_inference_policy(device=device)
137-
env.set_autoreset(False)
138128

139129
state: A2ArmPosForceState = env.command_manager.get_term("task_state")
140130
teleop = make_teleop_from_state(state)

scripts/train_cse_ppo.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@
4646
EXPORT_POLICY = False
4747

4848

49-
def _algo_config_dict(cfg: DictConfig) -> dict[str, Any]:
50-
"""Compatibility helper matching the historical CSE entrypoint surface."""
51-
return algo_config_dict(cfg)
52-
53-
5449
def _backend_adapter(cfg: DictConfig) -> BackendAdapter:
5550
return BackendAdapter(
5651
cfg,
@@ -64,30 +59,14 @@ def _get_log_root(cfg: DictConfig) -> str:
6459
return str(get_log_root(ROOT_DIR, cfg))
6560

6661

67-
def _format_play_checkpoint_error(
68-
cfg: DictConfig,
69-
*,
70-
task_log_root: Path,
71-
load_path: Path | None,
72-
load_path_dir: Path | None,
73-
) -> str:
74-
"""Historical CSE helper retained while delegating to shared diagnostics."""
75-
return format_play_checkpoint_error(
76-
cfg,
77-
task_log_root=task_log_root,
78-
load_path=load_path,
79-
load_path_dir=load_path_dir,
80-
)
81-
82-
8362
def play_cse_ppo(cfg: DictConfig, device: str) -> str | None:
8463
"""Resolve a checkpoint and render a CSE policy using the shared playback session."""
85-
rl_cfg = _algo_config_dict(cfg)
64+
rl_cfg = algo_config_dict(cfg)
8665
task_log_root = get_log_root(ROOT_DIR, cfg) / str(cfg.training.task_name)
8766
load_path, load_path_dir = parse_checkpoint_path(cfg, root_dir=ROOT_DIR)
8867
if load_path is None or load_path_dir is None or not load_path.exists():
8968
print(
90-
_format_play_checkpoint_error(
69+
format_play_checkpoint_error(
9170
cfg, task_log_root=task_log_root, load_path=load_path, load_path_dir=load_path_dir
9271
)
9372
)
@@ -161,9 +140,6 @@ def env_factory(num_envs: int):
161140
"cam_tracking_env_idx": getattr(cfg.training, "cam_tracking_env_idx", 0),
162141
"cam_tracking_extra_envs": getattr(cfg.training, "cam_tracking_extra_envs", 2),
163142
},
164-
extra_data_getter=(lambda: getattr(env, "curr_ee_goal_world", None))
165-
if hasattr(env, "curr_ee_goal_world")
166-
else None,
167143
)
168144
return str(output_video)
169145

@@ -209,7 +185,7 @@ def main(cfg: DictConfig) -> None:
209185
apply_env_nan_guard(env, cfg.training)
210186
runner = CSEOnPolicyRunner(
211187
RslRlVecEnvWrapper(env, device=device),
212-
_algo_config_dict(cfg),
188+
algo_config_dict(cfg),
213189
log_dir=log_dir,
214190
device=device,
215191
)

src/unilab/algos/cse_ppo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
from .runner import CSEOnPolicyRunner
77
from .storage import CSERolloutStorage
88

9-
__all__ = ["CSEActorCritic", "CSEPPO", "CSEEstimator", "CSEOnPolicyRunner", "CSERolloutStorage"]
9+
__all__ = ["CSEPPO", "CSEActorCritic", "CSEEstimator", "CSEOnPolicyRunner", "CSERolloutStorage"]

src/unilab/algos/cse_ppo/actor_critic.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
from __future__ import annotations
55

6-
from typing import Sequence
6+
from collections.abc import Sequence
77

88
import torch
99
from torch import nn
1010
from torch.distributions import Normal
1111

12-
from .estimator import CSEEstimator, get_activation
12+
from .estimator import CSEEstimator, _mlp
1313

1414

1515
class CSEActorCritic(nn.Module):
@@ -48,13 +48,13 @@ def __init__(
4848
activation=activation,
4949
**dict(estimator or {}),
5050
)
51-
self.actor = _build_mlp(
51+
self.actor = _mlp(
5252
self.num_one_step_obs + self.estimator.num_latent,
5353
self.num_actions,
5454
actor_hidden_dims,
5555
activation,
5656
)
57-
self.critic = _build_mlp(self.num_critic_obs, 1, critic_hidden_dims, activation)
57+
self.critic = _mlp(self.num_critic_obs, 1, critic_hidden_dims, activation)
5858
self.std = nn.Parameter(float(init_noise_std) * torch.ones(self.num_actions))
5959
self.distribution: Normal | None = None
6060
Normal.set_default_validate_args(False)
@@ -105,19 +105,3 @@ def act_inference(self, obs_history: torch.Tensor, observations=None) -> torch.T
105105
def evaluate(self, critic_observations: torch.Tensor, **kwargs) -> torch.Tensor:
106106
del kwargs
107107
return self.critic(critic_observations)
108-
109-
110-
def _build_mlp(
111-
input_dim: int,
112-
output_dim: int,
113-
hidden_dims: Sequence[int],
114-
activation: str,
115-
) -> nn.Sequential:
116-
"""Build a plain feed-forward MLP (kept as a module-level compatibility helper)."""
117-
layers: list[nn.Module] = []
118-
last = int(input_dim)
119-
for dim in hidden_dims:
120-
layers.extend((nn.Linear(last, int(dim)), get_activation(activation)))
121-
last = int(dim)
122-
layers.append(nn.Linear(last, int(output_dim)))
123-
return nn.Sequential(*layers)

src/unilab/algos/cse_ppo/algorithm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def update(self) -> tuple[float, float, float]:
226226
def _critic_obs(obs: TensorDict | torch.Tensor) -> torch.Tensor:
227227
if isinstance(obs, TensorDict):
228228
for key in ("critic", "policy", "actor"):
229-
if key in obs.keys():
229+
if key in obs:
230230
return obs[key]
231231
raise KeyError("CSE-PPO TensorDict obs must contain critic, policy, or actor")
232232
return obs

src/unilab/algos/cse_ppo/estimator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _regression_loss(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Te
121121
return (weights * F.mse_loss(pred, target, reduction="none")).mean()
122122
loss = pred.new_zeros(())
123123
offset = 0
124-
for size, weight in zip(self.target_group_sizes, weights):
124+
for size, weight in zip(self.target_group_sizes, weights, strict=True):
125125
part = slice(offset, offset + size)
126126
loss = loss + F.mse_loss(pred[:, part] * weight, target[:, part] * weight)
127127
offset += size

0 commit comments

Comments
 (0)