Skip to content

Commit a877c6e

Browse files
authored
Merge pull request #103 from UT-Austin-RPL/zs
Env Updates
2 parents 2d85600 + c32dfe0 commit a877c6e

9 files changed

Lines changed: 192 additions & 60 deletions

File tree

amago/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def get_actions(
763763
actions = action_dists.mean
764764
# get intended gamma distribution (always in -1 idx)
765765
actions = actions[..., -1, :]
766-
dtype = torch.uint8 if (self.discrete or self.multibinary) else torch.float32
766+
dtype = torch.int64 if (self.discrete or self.multibinary) else torch.float32
767767
return actions.to(dtype=dtype), hidden_state
768768

769769
@torch.no_grad()

amago/envs/builtin/half_cheetah_v4_vel.py

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"""
44

55
import random
6+
from typing import List
67

8+
import gymnasium as gym
79
from gymnasium.envs.mujoco.half_cheetah_v4 import HalfCheetahEnv
810
import numpy as np
911

@@ -37,7 +39,19 @@ def step(self, action):
3739

3840

3941
class HalfCheetahV4LogVelocity(_HalfCheetahV4ExposeVelReward):
40-
"""A wrapper around HalfCheetah-V4 that will automatically log velocity metrics when used in AMAGO."""
42+
"""A wrapper around HalfCheetah-V4 that will automatically log velocity metrics when used in AMAGO.
43+
44+
Args:
45+
max_episode_steps: Step horizon at which the inner episode is
46+
truncated. Default 1000 matches the original gym registration.
47+
Lower values are useful for k-episode meta-trial wrappers that
48+
want to fit multiple inner episodes inside a fixed sequence
49+
length.
50+
"""
51+
52+
def __init__(self, max_episode_steps: int = 1000, **kwargs):
53+
super().__init__(**kwargs)
54+
self.max_episode_steps = int(max_episode_steps)
4155

4256
def reset(self, *args, **kwargs):
4357
obs, info = super().reset(*args, **kwargs)
@@ -51,7 +65,7 @@ def step(self, action):
5165
# gym registration that would normally handle it.
5266
self.step_count += 1
5367
self._velocity_history.append(info["x_velocity"])
54-
truncated = truncated or self.step_count >= 1000
68+
truncated = truncated or self.step_count >= self.max_episode_steps
5569
if terminated or truncated:
5670
v = np.array(self._velocity_history)
5771
info[f"{AMAGO_ENV_LOG_PREFIX} Mean Velocity"] = v.mean().item()
@@ -69,6 +83,17 @@ class HalfCheetahV4_MetaVelocity(HalfCheetahV4LogVelocity):
6983
Reward terms are based on the version featured in the VariBAD codebase
7084
(https://github.com/lmzintgraf/varibad/blob/57e1795be142ace52d0c353097acf193d9067200/environments/mujoco/half_cheetah_vel.py#L8)
7185
86+
A single hidden ``target_velocity`` persists across ``k_episodes`` inner
87+
episodes (each truncated by the parent class at ``max_episode_steps``).
88+
Between inner episodes the simulator is soft-reset; the hidden task is
89+
only resampled when ``reset(new_task=True)`` is called from outside (the
90+
default at the start of every meta-trial). A soft-reset flag (0/1) is
91+
appended to the obs so the agent can detect inner-episode boundaries
92+
without losing task identity. Per-episode metrics from the parent class
93+
(Mean / Max / Avg. Last 50 Timestep Velocity) are promoted to
94+
``Trial {i} ...`` keys, matching the meta-RL logging convention used by
95+
``KEpisodeMetaworld``.
96+
7297
Args:
7398
ctrl_cost_weight: Defaults to half the normal ctrl cost, as in the original HalfCheetahVelocity task.
7499
velocity_reward_weight: Defaults to 1.0.
@@ -80,6 +105,10 @@ class HalfCheetahV4_MetaVelocity(HalfCheetahV4LogVelocity):
80105
81106
task_min_velocity: Minimum target velocity that determines the hidden task. Defaults to 0.0.
82107
task_max_velocity: Maximum target velocity that determines the hidden task. Defaults to 3.0.
108+
k_episodes: Number of inner episodes per meta-trial. Default 1
109+
recovers the unwrapped single-episode task. Set ``>=2`` for
110+
true meta-RL trials (e.g. 3 inner episodes of 200 steps with
111+
``max_episode_steps=200``).
83112
"""
84113

85114
def __init__(
@@ -93,6 +122,7 @@ def __init__(
93122
# We can use the HalfCheetahV4LogVelocity env above to verify this.
94123
task_min_velocity: float = 0.0,
95124
task_max_velocity: float = 3.0,
125+
k_episodes: int = 1,
96126
**kwargs,
97127
):
98128
super().__init__(
@@ -102,8 +132,24 @@ def __init__(
102132
)
103133
self.task_min_velocity = task_min_velocity
104134
self.task_max_velocity = task_max_velocity
135+
self.k_episodes = int(k_episodes)
136+
137+
# Append a soft-reset flag (0/1) to the parent observation space.
138+
flat = self.observation_space
139+
low = np.append(flat.low.astype(np.float32), np.float32(0.0))
140+
high = np.append(flat.high.astype(np.float32), np.float32(1.0))
141+
self.observation_space = gym.spaces.Box(low=low, high=high, dtype=np.float32)
142+
143+
self.target_velocity: float = 0.0
144+
self.current_trial: int = 0
145+
self._trial_returns: List[float] = []
146+
self._current_trial_return: float = 0.0
105147
self.reset()
106148

149+
@staticmethod
150+
def _augment(obs: np.ndarray, soft_reset: bool) -> np.ndarray:
151+
return np.append(obs, float(soft_reset)).astype(np.float32)
152+
107153
def velocity_reward_term(self, x_velocity):
108154
return (
109155
self._forward_reward_weight
@@ -117,16 +163,64 @@ def sample_target_velocity(self):
117163
# tasks are different across async parallel actors!
118164
return random.uniform(self.task_min_velocity, self.task_max_velocity)
119165

120-
def reset(self, *args, **kwargs):
121-
obs, info = super().reset(*args, **kwargs)
122-
self.target_velocity = self.sample_target_velocity()
123-
return obs, info
166+
def reset(self, *, new_task: bool = True, seed=None, options=None):
167+
# ``new_task=False`` is reserved for soft resets *inside* step();
168+
# external callers should use the default to start a fresh meta-trial.
169+
obs, info = super().reset(seed=seed, options=options)
170+
if new_task:
171+
self.target_velocity = self.sample_target_velocity()
172+
self.current_trial = 0
173+
self._trial_returns = []
174+
self._current_trial_return = 0.0
175+
return self._augment(obs, soft_reset=True), info
176+
177+
def _log_per_trial_metrics(self, info: dict) -> None:
178+
ti = self.current_trial
179+
for suffix in (
180+
"Mean Velocity",
181+
"Max Velocity",
182+
"Avg. Last 50 Timestep Velocity",
183+
):
184+
src = f"{AMAGO_ENV_LOG_PREFIX} {suffix}"
185+
if src in info:
186+
info[f"{AMAGO_ENV_LOG_PREFIX} Trial {ti} {suffix}"] = info.pop(src)
187+
achieved_key = (
188+
f"{AMAGO_ENV_LOG_PREFIX} Trial {ti} Avg. Last 50 Timestep Velocity"
189+
)
190+
if achieved_key in info:
191+
info[
192+
f"{AMAGO_ENV_LOG_PREFIX} Trial {ti} Target Velocity Error at Final 50 Timesteps"
193+
] = abs(self.target_velocity - info[achieved_key])
194+
info[f"{AMAGO_ENV_LOG_PREFIX} Trial {ti} Return"] = self._current_trial_return
124195

125196
def step(self, action):
126197
obs, reward, terminated, truncated, info = super().step(action)
198+
self._current_trial_return += float(reward)
199+
soft_reset = False
200+
127201
if terminated or truncated:
128-
achieved = info[f"{AMAGO_ENV_LOG_PREFIX} Avg. Last 50 Timestep Velocity"]
129-
info[
130-
f"{AMAGO_ENV_LOG_PREFIX} Target Velocity Error at Final 50 Timesteps"
131-
] = abs(self.target_velocity - achieved)
132-
return obs, reward, terminated, truncated, info
202+
self._log_per_trial_metrics(info)
203+
self._trial_returns.append(self._current_trial_return)
204+
self.current_trial += 1
205+
206+
# Always soft-reset the simulator on inner-episode end (mirrors
207+
# KEpisodeMetaworld). The augmented obs's soft-reset flag is set
208+
# to 1 so the agent can detect the boundary.
209+
obs, _ = super().reset()
210+
soft_reset = True
211+
self._current_trial_return = 0.0
212+
213+
if self.current_trial >= self.k_episodes:
214+
# Meta-trial end is a time-limit truncation, not a terminal
215+
# state: the underlying MDP could continue if we kept
216+
# observing. Leave terminated=False so the critic
217+
# bootstraps as usual.
218+
terminated = False
219+
truncated = True
220+
info[f"{AMAGO_ENV_LOG_PREFIX} Total Return"] = float(
221+
np.sum(self._trial_returns)
222+
)
223+
else:
224+
terminated = truncated = False
225+
226+
return self._augment(obs, soft_reset), reward, terminated, truncated, info

amago/envs/builtin/metaworld_ml.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ class Metaworld(AMAGOEnv):
3333
k_episodes: The number of attempts the policy has to adapt to the current task.
3434
"""
3535

36-
def __init__(self, benchmark_name: str, split: str, k_episodes: int):
36+
def __init__(
37+
self,
38+
benchmark_name: str,
39+
split: str,
40+
k_episodes: int,
41+
max_episode_length: int = 500,
42+
):
3743
if benchmark_name == "ml10":
3844
benchmark = metaworld.ML10()
3945
elif benchmark_name == "ml45":
@@ -44,7 +50,7 @@ def __init__(self, benchmark_name: str, split: str, k_episodes: int):
4450
except:
4551
raise ValueError(f"Unrecognized metaworld benchmark {benchmark_name}")
4652

47-
env = KEpisodeMetaworld(benchmark, split, k_episodes)
53+
env = KEpisodeMetaworld(benchmark, split, k_episodes, max_episode_length)
4854
super().__init__(
4955
env=env,
5056
env_name=f"metaworld_{benchmark_name}",

amago/envs/env_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def action(self, action):
141141
if isinstance(action, int):
142142
return action
143143
elif isinstance(action, np.ndarray) and action.ndim == 1:
144-
assert action.shape[0] == 1 and action.dtype == np.uint8
144+
assert action.shape[0] == 1
145145
return action[0].item()
146146
elif isinstance(action, np.ndarray) and action.ndim == 2:
147147
assert action.shape[1] == 1

amago/envs/exploration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def add_exploration_noise(self, action: np.ndarray, local_step: np.ndarray):
205205
use_random = np.expand_dims(self.rng.random(self.batched_envs) <= noise, 1)
206206
expl_action = (
207207
use_random * random_action + (1 - use_random) * action
208-
).astype(np.uint8)
208+
).astype(np.int64)
209209
assert expl_action.shape == (self.batched_envs, 1)
210210
else:
211211
# random noise (TD3-style)

amago/experiment.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class Experiment:
7777
:param wandb_group_name: Group runs on wandb dashboard. **Default:** None.
7878
:param verbose: Print tqdm bars and info to console. **Default:** True.
7979
:param log_interval: Log extra metrics every N batches. **Default:** 300.
80-
:param padded_sampling: Padding for sampling training subsequences. "none", "left", "right", "both". **Default:** "none".
80+
:param padded_sampling: Padding for sampling training subsequences. "none", "left", "right", "both", "center". **Default:** "none".
8181
:param dloader_workers: Number of DataLoader workers for disk loading. Increase for compressed/large trajs.
8282
8383
.. note::
@@ -522,6 +522,15 @@ def policy(self) -> BaseAgent:
522522
"""Returns the current Agent policy free from the accelerator wrapper."""
523523
return self.accelerator.unwrap_model(self.policy_aclr)
524524

525+
def on_interact_step(self, obs: dict) -> None:
526+
"""Hook called before each action during ``self.interact(...)``.
527+
528+
Override in a subclass to mutate agent state between steps
529+
(e.g. resample ``agent._inference_alpha`` at episode boundaries by
530+
inspecting ``obs["episode_id"]``). No-op in the base implementation.
531+
"""
532+
pass
533+
525534
def interact(
526535
self,
527536
envs,
@@ -603,6 +612,7 @@ def get_t():
603612
obs, rl2s, time_idxs = get_t()
604613
episodes_finished = 0
605614
for _ in iter_:
615+
self.on_interact_step(obs)
606616
with torch.no_grad():
607617
with self.caster():
608618
actions, hidden_state = policy.get_actions(

amago/loading.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def random_slice(self, length: int, padded_sampling: str = "none"):
9090
where the start of the training sequence is not always the first timestep of the trajectory.
9191
"right" --> sample while effectively padding the right side of the sequence for cases
9292
where the end of the trajectory may be undersampled.
93+
"center" --> uniform random window center, clamped to valid range.
94+
Always returns a full-length (never padded) window. Fixes endpoint
95+
undersampling of "none" when the trajectory is longer than `length`.
9396
"""
9497
if len(self) <= length:
9598
start = 0
@@ -101,6 +104,9 @@ def random_slice(self, length: int, padded_sampling: str = "none"):
101104
start = self._safe_randrange(-length + 1, len(self) - length + 1)
102105
elif padded_sampling == "right":
103106
start = self._safe_randrange(0, len(self) - 1)
107+
elif padded_sampling == "center":
108+
center = self._safe_randrange(0, len(self))
109+
start = min(max(center - length // 2, 0), len(self) - length)
104110
else:
105111
raise ValueError(
106112
f"Unrecognized `padded_sampling` mode: `{padded_sampling}`"

examples/07_metaworld.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def add_cli(parser):
3838
"amago.nets.actor_critic.NCriticsTwoHot.min_return": -100.0,
3939
"amago.nets.actor_critic.NCriticsTwoHot.max_return": 5000 * args.k,
4040
"amago.nets.actor_critic.NCriticsTwoHot.output_bins": 96,
41+
#"amago.nets.traj_encoders.TformerTrajEncoder.pos_emb": "rope",
42+
"amago.nets.actor_critic.NCriticsTwoHot.d_hidden": 300,
4143
}
4244
traj_encoder_type = cli_utils.switch_traj_encoder(
4345
config,
@@ -49,12 +51,16 @@ def add_cli(parser):
4951
config, args.agent_type, reward_multiplier=1.0, num_critics=4
5052
)
5153
exploration_type = cli_utils.switch_exploration(
52-
config, "bilevel", steps_anneal=2_000_000, rollout_horizon=args.k * 500
54+
config, "bilevel", steps_anneal=2_000_000, rollout_horizon=args.k * 300
5355
)
5456
cli_utils.use_config(config, args.configs)
5557

56-
make_train_env = lambda: Metaworld(args.benchmark, "train", k_episodes=args.k)
57-
make_test_env = lambda: Metaworld(args.benchmark, "test", k_episodes=args.k)
58+
make_train_env = lambda: Metaworld(
59+
args.benchmark, "train", k_episodes=args.k, max_episode_length=300
60+
)
61+
make_test_env = lambda: Metaworld(
62+
args.benchmark, "test", k_episodes=args.k, max_episode_length=300
63+
)
5864

5965
group_name = (
6066
f"{args.run_name}_metaworld_{args.benchmark}_K_{args.k}_L_{args.max_seq_len}"
@@ -66,13 +72,13 @@ def add_cli(parser):
6672
make_train_env=make_train_env,
6773
make_val_env=make_train_env,
6874
max_seq_len=args.max_seq_len,
69-
traj_save_len=min(500 * args.k + 1, args.max_seq_len * 4),
75+
traj_save_len=min(300 * args.k + 1, args.max_seq_len * 4),
7076
group_name=group_name,
7177
run_name=run_name,
7278
tstep_encoder_type=FFTstepEncoder,
7379
traj_encoder_type=traj_encoder_type,
7480
agent_type=agent_type,
75-
val_timesteps_per_epoch=15 * args.k * 500 + 1,
81+
val_timesteps_per_epoch=15 * args.k * 300 + 1,
7682
learning_rate=5e-4,
7783
grad_clip=2.0,
7884
exploration_wrapper_type=exploration_type,

0 commit comments

Comments
 (0)