Skip to content

Commit 3b5dd92

Browse files
authored
Merge pull request #44 from Mingrui-Yu/main
APPO works in AllegroInhandRotation
2 parents 8929924 + aecb22f commit 3b5dd92

8 files changed

Lines changed: 171 additions & 34 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"rich",
2626
"typing-extensions",
2727
"mlx ; sys_platform == 'darwin'",
28+
"lark>=1.3.1",
2829
]
2930

3031
[project.optional-dependencies]

‎scripts/train_appo.py‎

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,21 @@
1515

1616

1717
def ensure_registries():
18-
try:
19-
import unilab.envs.locomotion
20-
21-
package = unilab.envs.locomotion
22-
if hasattr(package, "__path__"):
23-
for _, name, ispkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
24-
try:
25-
importlib.import_module(name)
26-
except Exception:
27-
pass
28-
except ImportError:
29-
pass
30-
31-
try:
32-
import unilab.envs.locomotion.walking
33-
34-
package = unilab.envs.locomotion.walking
35-
if hasattr(package, "__path__"):
36-
for _, name, ispkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
37-
try:
38-
importlib.import_module(name)
39-
except Exception:
40-
pass
41-
except ImportError:
42-
pass
18+
for pkg_name in (
19+
"unilab.envs.locomotion",
20+
"unilab.envs.locomotion.walking",
21+
"unilab.envs.manipulation",
22+
):
23+
try:
24+
package = importlib.import_module(pkg_name)
25+
if hasattr(package, "__path__"):
26+
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
27+
try:
28+
importlib.import_module(name)
29+
except Exception:
30+
pass
31+
except ImportError:
32+
pass
4333

4434

4535
def play_appo(args, rl_cfg):
@@ -219,9 +209,12 @@ def main():
219209

220210
ensure_registries()
221211

222-
from unilab.config.locomotion_params import appo_config
212+
from unilab.config import locomotion_params, manipulation_params
223213

224-
rl_cfg = appo_config(args.task)
214+
if args.task in manipulation_params.KNOWN_TASKS:
215+
rl_cfg = manipulation_params.appo_config(args.task)
216+
else:
217+
rl_cfg = locomotion_params.appo_config(args.task)
225218

226219
# Fall back to config values for args not explicitly provided on the CLI.
227220
if args.max_iterations is None:

‎src/unilab/algos/torch/appo/runner.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def learn(
179179

180180
weight_param_shapes = {name: p.shape for name, p in learner.actor.state_dict().items()}
181181

182-
metrics_queue: mp.Queue = mp.Queue(maxsize=100)
182+
metrics_queue: mp.Queue = mp.get_context("spawn").Queue(maxsize=100)
183183

184184
# Start collector
185185
collector_kwargs = {

‎src/unilab/config/locomotion_params.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def appo_config(env_name: str) -> config_dict.ConfigDict:
115115
steps_per_env=24,
116116
max_iterations=150,
117117
save_interval=50,
118+
empirical_normalization=False,
118119
)
119120

120121
if env_name == "Go1JoystickFlatTerrain":
@@ -133,13 +134,15 @@ def appo_config(env_name: str) -> config_dict.ConfigDict:
133134
steps_per_env=cfg.steps_per_env,
134135
max_iterations=cfg.max_iterations,
135136
save_interval=cfg.save_interval,
137+
empirical_normalization=cfg.empirical_normalization,
136138
obs_groups=config_dict.create(
137139
actor=config_dict.create(policy=0),
138140
),
139141
actor=config_dict.create(
140142
class_name="rsl_rl.models.MLPModel",
141143
hidden_dims=[512, 256, 128],
142144
activation="elu",
145+
obs_normalization=cfg.empirical_normalization,
143146
distribution_cfg=config_dict.create(
144147
class_name="rsl_rl.modules.distribution.GaussianDistribution",
145148
init_std=1.0,
@@ -150,6 +153,7 @@ def appo_config(env_name: str) -> config_dict.ConfigDict:
150153
class_name="rsl_rl.models.MLPModel",
151154
hidden_dims=[512, 256, 128],
152155
activation="elu",
156+
obs_normalization=cfg.empirical_normalization,
153157
),
154158
algorithm=config_dict.create(
155159
num_learning_epochs=5,

‎src/unilab/config/manipulation_params.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,86 @@ def ppo_config(env_name: str) -> config_dict.ConfigDict:
9090
)
9191

9292

93+
def appo_config(env_name: str) -> config_dict.ConfigDict:
94+
"""Return APPO config.
95+
96+
Mirrors the structure of ppo_config/offpolicy_config so training
97+
infrastructure is reusable. Algorithm hyperparams live under the
98+
``algorithm`` sub-dict; network architecture under ``actor``/``critic``.
99+
"""
100+
cfg = config_dict.create(
101+
seed=1,
102+
num_envs=2048,
103+
steps_per_env=24,
104+
max_iterations=150,
105+
save_interval=50,
106+
value_loss_coef=1.0,
107+
desired_kl=0.01,
108+
empirical_normalization=False,
109+
)
110+
111+
if env_name == "AllegroInhandRotation":
112+
cfg.num_envs = 16384
113+
cfg.max_iterations = 501
114+
cfg.steps_per_env = 8
115+
cfg.value_loss_coef = 4.0
116+
cfg.desired_kl = 0.02
117+
cfg.save_interval = 100
118+
cfg.empirical_normalization = True
119+
elif env_name == "AllegroInhandRotationSac":
120+
raise NotImplementedError()
121+
122+
return config_dict.create(
123+
algo="appo",
124+
algo_log_name="appo",
125+
seed=cfg.seed,
126+
num_envs=cfg.num_envs,
127+
steps_per_env=cfg.steps_per_env,
128+
max_iterations=cfg.max_iterations,
129+
save_interval=cfg.save_interval,
130+
empirical_normalization=cfg.empirical_normalization,
131+
obs_groups=config_dict.create(
132+
actor=config_dict.create(policy=0),
133+
),
134+
actor=config_dict.create(
135+
class_name="rsl_rl.models.MLPModel",
136+
hidden_dims=[512, 256, 128],
137+
activation="elu",
138+
obs_normalization=cfg.empirical_normalization,
139+
distribution_cfg=config_dict.create(
140+
class_name="rsl_rl.modules.distribution.GaussianDistribution",
141+
init_std=1.0,
142+
std_type="scalar",
143+
),
144+
),
145+
critic=config_dict.create(
146+
class_name="rsl_rl.models.MLPModel",
147+
hidden_dims=[512, 256, 128],
148+
activation="elu",
149+
obs_normalization=cfg.empirical_normalization,
150+
),
151+
algorithm=config_dict.create(
152+
num_learning_epochs=5,
153+
num_mini_batches=4,
154+
clip_param=0.2,
155+
gamma=0.99,
156+
lam=0.95,
157+
value_loss_coef=cfg.value_loss_coef,
158+
entropy_coef=0.01,
159+
learning_rate=1e-3,
160+
max_grad_norm=1.0,
161+
use_clipped_value_loss=True,
162+
schedule="adaptive",
163+
desired_kl=cfg.desired_kl,
164+
optimizer="adam",
165+
tau=1.0,
166+
target_update_freq=1,
167+
vtrace_clip_rho=1.0,
168+
vtrace_clip_c=1.0,
169+
),
170+
)
171+
172+
93173
def offpolicy_config(algo: str, env_name: str) -> config_dict.ConfigDict:
94174
"""Return a unified off-policy config schema for SAC/TD3.
95175

‎src/unilab/envs/manipulation/inhand_rot_allegro/rotation.py‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ class RewardConfig:
3636
reset_z_threshold: float = 0.125 # metres
3737

3838

39+
@dataclass
40+
class obs_cfg:
41+
obs_dict = {
42+
"dof_pos_norm": 16,
43+
"targets": 16,
44+
"ball_pos": 3,
45+
} # 'obs_name': dim
46+
actor_obs = ["dof_pos_norm", "targets", "ball_pos"]
47+
num_lag_steps: int = 3
48+
49+
3950
@dataclass
4051
class DomainRandConfig:
4152
# Small random perturbation added to each joint at reset (rad).
@@ -53,6 +64,7 @@ class AllegroRotationCfg(AllegroBaseCfg):
5364
max_episode_seconds: float = 20.0 # same as HORA
5465
reward_config: RewardConfig = field(default_factory=RewardConfig)
5566
domain_rand: DomainRandConfig = field(default_factory=DomainRandConfig)
67+
obs_config: obs_cfg = field(default_factory=obs_cfg)
5668

5769
# World-frame unit vector around which to reward rotation.
5870
# Default (+z) = counterclockwise when viewed from above.
@@ -96,10 +108,27 @@ def __init__(self, cfg: AllegroRotationCfg, num_envs: int = 1, backend_type: str
96108
# ── Spaces ──────────────────────────────────────────────────────
97109

98110
def _init_obs_space(self):
99-
# Last 3 timesteps of [dof_pos_norm(16) + targets(16) + ball_pos(3)] = 3 * 35 = 105
111+
obs_cfg = self._cfg.obs_config
112+
num_obs_per_step = sum(obs_cfg.obs_dict.values())
113+
num_obs = obs_cfg.num_lag_steps * num_obs_per_step
100114
self._observation_space = gym.spaces.Box(
101-
low=-np.inf, high=np.inf, shape=(105,), dtype=float
115+
low=-np.inf, high=np.inf, shape=(num_obs,), dtype=float
102116
)
117+
self.actor_indices = self._get_actor_indices()
118+
119+
def _get_actor_indices(self):
120+
s = 0
121+
indices = []
122+
for i in self._cfg.obs_config.actor_obs:
123+
s = 0
124+
for k, v in self._cfg.obs_config.obs_dict.items():
125+
if k == i:
126+
for q in range(s, s + v):
127+
indices.append(q)
128+
s += v
129+
break
130+
s += v
131+
return indices
103132

104133
@property
105134
def observation_space(self) -> gym.spaces.Box:
@@ -212,7 +241,15 @@ def update_state(self, state: NpEnvState) -> NpEnvState:
212241
current_obs = np.concatenate([dof_pos_norm, targets, ball_pos], axis=1) # (N, 35)
213242

214243
obs_lag_history = state.info.get(
215-
"obs_lag_history", np.zeros((self._num_envs, 3, 35), dtype=self._np_dtype)
244+
"obs_lag_history",
245+
np.zeros(
246+
(
247+
self._num_envs,
248+
self._cfg.obs_config.num_lag_steps,
249+
sum(self._cfg.obs_config.obs_dict.values()),
250+
),
251+
dtype=self._np_dtype,
252+
),
216253
)
217254
obs_lag_history[:, :-1] = obs_lag_history[:, 1:]
218255
obs_lag_history[:, -1] = current_obs
@@ -249,8 +286,11 @@ def _compute_rewards(self, state: NpEnvState) -> tuple:
249286

250287
def _get_obs(self, info: dict) -> np.ndarray:
251288
"""Extract observation from info. Pure function - does not modify state."""
289+
num_obs_per_step = sum(self._cfg.obs_config.obs_dict.values())
290+
num_lag_steps = self._cfg.obs_config.num_lag_steps
252291
obs_lag_history = info.get(
253-
"obs_lag_history", np.zeros((self._num_envs, 3, 35), dtype=self._np_dtype)
292+
"obs_lag_history",
293+
np.zeros((self._num_envs, num_lag_steps, num_obs_per_step), dtype=self._np_dtype),
254294
)
255295
num_envs = obs_lag_history.shape[0]
256296
return np.asarray(obs_lag_history.reshape(num_envs, -1))
@@ -323,8 +363,13 @@ def reset(self, env_indices: np.ndarray) -> Tuple[np.ndarray, np.ndarray, dict]:
323363
[dof_pos_norm, init_ctrl, ball_pos_f32], axis=1
324364
) # (num_reset, 35)
325365
obs_lag_history = np.broadcast_to(
326-
init_obs[:, None, :], (num_reset, 3, 35)
327-
).copy() # (num_reset, 3, 35)
366+
init_obs[:, None, :],
367+
(
368+
num_reset,
369+
self._cfg.obs_config.num_lag_steps,
370+
sum(self._cfg.obs_config.obs_dict.values()),
371+
),
372+
).copy() # (num_reset, num_lag_steps, num_obs_per_step)
328373

329374
info = {
330375
"current_actions": np.zeros((num_reset, self._num_action), dtype=self._np_dtype),

‎src/unilab/utils/render_many.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def _resolve_gl_backend() -> str:
2828
import ctypes
2929

3030
ctypes.CDLL("libEGL.so.1")
31+
# NVIDIA EGL requires a device ID to initialize offscreen contexts in
32+
# spawned subprocesses; default to GPU 0 if the user hasn't set it.
33+
os.environ.setdefault("MUJOCO_EGL_DEVICE_ID", "0")
3134
return "egl"
3235
except OSError:
3336
return "glfw"

‎uv.lock‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)