181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""DiscoRL-compatible CartPole environment wrapper.
|
|
|
|
This module:
|
|
1. Wraps standard Gym CartPole in DiscoRL's Environment interface
|
|
2. CartPole naturally has discrete actions (0 or 1)
|
|
3. Provides flexible observation/action preprocessing
|
|
|
|
The design supports:
|
|
- Simple batch handling (Python-level, non-JAX)
|
|
- Discrete action space (required by DiscoRL Agent)
|
|
- Standard Gym interface (reset/step)
|
|
"""
|
|
|
|
from typing import Any, Dict, Tuple, Optional
|
|
import numpy as np
|
|
import jax
|
|
import jax.numpy as jnp
|
|
import gymnasium as gym
|
|
|
|
from disco_rl.environments import base
|
|
from disco_rl import types
|
|
|
|
try:
|
|
from dm_env import StepType
|
|
except ImportError:
|
|
# Fallback with correct mapping
|
|
class StepType:
|
|
FIRST = 0
|
|
MID = 1
|
|
LAST = 2
|
|
|
|
|
|
class DiscoCartPoleEnv(base.Environment):
|
|
"""DiscoRL-compatible batched CartPole environment.
|
|
|
|
CartPole already has discrete actions (0, 1), so no discretization needed.
|
|
This adapter simply wraps Gym CartPole to provide DiscoRL's Environment interface
|
|
with types.EnvironmentTimestep.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
batch_size: int = 1,
|
|
max_steps: int = 500,
|
|
):
|
|
self.batch_size = batch_size
|
|
self.max_steps = max_steps
|
|
self._step_counts = np.zeros(batch_size, dtype=np.int32)
|
|
self._episode_done = np.zeros(batch_size, dtype=bool)
|
|
|
|
# Create env instances
|
|
self._envs = [gym.make('CartPole-v1') for _ in range(batch_size)]
|
|
|
|
# Build specs from first env
|
|
base_env = self._envs[0]
|
|
|
|
try:
|
|
from dm_env import specs as dm_specs
|
|
# CartPole has action space Discrete(2), so actions are {0, 1}
|
|
self._single_action_spec = dm_specs.BoundedArray(
|
|
shape=(), dtype=np.int32, minimum=0, maximum=1
|
|
)
|
|
obs_shape = base_env.observation_space.shape
|
|
obs_dtype = base_env.observation_space.dtype
|
|
self._single_observation_spec = {
|
|
'observation': dm_specs.Array(shape=obs_shape, dtype=obs_dtype)
|
|
}
|
|
except Exception:
|
|
self._single_action_spec = type('ActionSpec', (), {
|
|
'shape': (),
|
|
'dtype': np.int32,
|
|
'low': 0,
|
|
'high': 1,
|
|
})
|
|
self._single_observation_spec = {
|
|
'observation': base_env.observation_space
|
|
}
|
|
|
|
self._last_obs = [None] * batch_size
|
|
self._last_info = [{}] * batch_size
|
|
|
|
def single_action_spec(self):
|
|
return self._single_action_spec
|
|
|
|
def single_observation_spec(self):
|
|
return self._single_observation_spec
|
|
|
|
def step(
|
|
self, state_unused: Any, actions: np.ndarray
|
|
) -> Tuple[Any, types.EnvironmentTimestep]:
|
|
"""Step all envs.
|
|
|
|
Args:
|
|
state_unused: unused (kept for DiscoRL interface compatibility)
|
|
actions: array of shape (batch_size,) with discrete action indices (0 or 1)
|
|
|
|
Returns:
|
|
(state, timestep) where timestep is a batched EnvironmentTimestep
|
|
"""
|
|
# Convert actions to list if needed
|
|
if isinstance(actions, (np.ndarray, jnp.ndarray)):
|
|
actions_list = [int(a) for a in np.asarray(actions)]
|
|
else:
|
|
actions_list = list(actions)
|
|
|
|
obs_batch = []
|
|
reward_batch = []
|
|
done_batch = []
|
|
|
|
for i, env in enumerate(self._envs):
|
|
action = actions_list[i] if i < len(actions_list) else 0
|
|
# Action should be 0 or 1 for CartPole
|
|
action = int(action) % 2
|
|
|
|
# ✅ FIX: Never auto-reset. Always step the environment.
|
|
# If episode is done, it should have been reset by the caller.
|
|
# This ensures correct reward propagation.
|
|
obs, reward, terminated, truncated, info = env.step(action)
|
|
done = bool(terminated or truncated)
|
|
|
|
# Increment step counter; mark as done on terminal or max steps
|
|
self._step_counts[i] += 1
|
|
if done or self._step_counts[i] >= self.max_steps:
|
|
self._episode_done[i] = True
|
|
|
|
self._last_obs[i] = obs
|
|
self._last_info[i] = info
|
|
obs_batch.append(jnp.asarray(obs, dtype=jnp.float32))
|
|
reward_batch.append(float(reward))
|
|
done_batch.append(done)
|
|
|
|
# Stack into batched timestep
|
|
obs_map = {'observation': jnp.stack(obs_batch)}
|
|
rewards = jnp.asarray(reward_batch, dtype=jnp.float32)
|
|
is_terminal = jnp.asarray(done_batch, dtype=jnp.bool_)
|
|
# Use LAST (2) for terminal, MID (1) for non-terminal
|
|
step_type = jnp.where(is_terminal, StepType.LAST, StepType.MID)
|
|
|
|
timestep = types.EnvironmentTimestep(
|
|
observation=obs_map,
|
|
step_type=step_type,
|
|
reward=rewards,
|
|
)
|
|
return None, timestep
|
|
|
|
def reset(self, rng_key: Optional[Any] = None) -> Tuple[Any, types.EnvironmentTimestep]:
|
|
"""Reset all envs.
|
|
|
|
Args:
|
|
rng_key: optional JAX RNG (unused here)
|
|
|
|
Returns:
|
|
(state, timestep)
|
|
"""
|
|
obs_batch = []
|
|
reward_batch = []
|
|
done_batch = []
|
|
|
|
for i, env in enumerate(self._envs):
|
|
obs, info = env.reset()
|
|
self._last_obs[i] = obs
|
|
self._last_info[i] = info
|
|
self._step_counts[i] = 0
|
|
self._episode_done[i] = False
|
|
obs_batch.append(jnp.asarray(obs, dtype=jnp.float32))
|
|
reward_batch.append(0.0)
|
|
done_batch.append(False)
|
|
|
|
obs_map = {'observation': jnp.stack(obs_batch)}
|
|
rewards = jnp.asarray(reward_batch, dtype=jnp.float32)
|
|
is_terminal = jnp.asarray(done_batch, dtype=jnp.bool_)
|
|
# Use FIRST (0) for reset, since this is the first step of a new episode
|
|
step_type = jnp.full((len(self._envs),), StepType.FIRST, dtype=jnp.int32)
|
|
|
|
timestep = types.EnvironmentTimestep(
|
|
observation=obs_map,
|
|
step_type=step_type,
|
|
reward=rewards,
|
|
)
|
|
return None, timestep
|