Skill Tracer
Skills › Scientific Agent Skills › stable-baselines3 › custom_environments.md

custom_environments.md

543 lines · skills/stable-baselines3/references/custom_environments.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Creating Custom Environments for Stable Baselines3
2
3Targets Gymnasium 1.3.0 and SB3 2.9.0. Partial classes/methods below are
4illustrative design fragments; the executable complete template is
5[custom_env_template.py](../scripts/custom_env_template.py). The template observes
6both agent and random goal, and the registered environment has a 100-step limit.
7A hidden random goal makes a different, partially observable problem; an API
8checker cannot detect that scientific error.
9

Creating Custom Environments for Stable Baselines3

Targets Gymnasium 1.3.0 and SB3 2.9.0. Partial classes/methods below are illustrative design fragments; the executable complete template is custom_env_template.py. The template observes both agent and random goal, and the registered environment has a 100-step limit. A hidden random goal makes a different, partially observable problem; an API checker cannot detect that scientific error.

Raw lines 1 to 9
Runs code scripts/custom_env_template.py :5
10## Environment Structure

Environment Structure

Raw lines 10 to 11
12### Required Methods
14Every custom environment must inherit from `gymnasium.Env` and implement:
16```python
17import gymnasium as gym
18from gymnasium import spaces
19import numpy as np
21class CustomEnv(gym.Env):
22 def __init__(self):
23 """Initialize environment, define action_space and observation_space"""
24 super().__init__()
25 self.action_space = spaces.Discrete(4)
26 self.observation_space = spaces.Box(low=0, high=1, shape=(4,), dtype=np.float32)
28 def reset(self, seed=None, options=None):
29 """Reset environment to initial state"""
30 super().reset(seed=seed)
31 observation = self.np_random.random(4).astype(np.float32)
32 info = {}
33 return observation, info
35 def step(self, action):
36 """Execute one timestep"""
37 observation = self.np_random.random(4).astype(np.float32)
38 reward = 0.0
39 terminated = False # Episode ended naturally
40 truncated = False # Episode ended due to time limit
41 info = {}
42 return observation, reward, terminated, truncated, info
44 def render(self):
45 """Visualize environment (optional)"""
46 pass
48 def close(self):
49 """Cleanup resources (optional)"""
50 pass
51```

Required Methods

Every custom environment must inherit from gymnasium.Env and implement:

import gymnasium as gym
from gymnasium import spaces
import numpy as np

class CustomEnv(gym.Env):
    def __init__(self):
        """Initialize environment, define action_space and observation_space"""
        super().__init__()
        self.action_space = spaces.Discrete(4)
        self.observation_space = spaces.Box(low=0, high=1, shape=(4,), dtype=np.float32)

    def reset(self, seed=None, options=None):
        """Reset environment to initial state"""
        super().reset(seed=seed)
        observation = self.np_random.random(4).astype(np.float32)
        info = {}
        return observation, info

    def step(self, action):
        """Execute one timestep"""
        observation = self.np_random.random(4).astype(np.float32)
        reward = 0.0
        terminated = False  # Episode ended naturally
        truncated = False   # Episode ended due to time limit
        info = {}
        return observation, reward, terminated, truncated, info

    def render(self):
        """Visualize environment (optional)"""
        pass

    def close(self):
        """Cleanup resources (optional)"""
        pass
Raw lines 12 to 52
53### Method Details
55#### `__init__(self, ...)`
57**Purpose:** Initialize the environment and define spaces.
59**Requirements:**
60- Must call `super().__init__()`
61- Must define `self.action_space`
62- Must define `self.observation_space`
64**Example:**
65```python
66def __init__(self, grid_size=10, max_steps=100):
67 super().__init__()
68 self.grid_size = grid_size
69 self.max_steps = max_steps
70 self.current_step = 0
72 # Define spaces
73 self.action_space = spaces.Discrete(4)
74 self.observation_space = spaces.Box(
75 low=0, high=grid_size-1, shape=(4,), dtype=np.float32
76 )
77```
79#### `reset(self, seed=None, options=None)`
81**Purpose:** Reset the environment to an initial state.
83**Requirements:**
84- Must call `super().reset(seed=seed)`
85- Must return `(observation, info)` tuple
86- Observation must match `observation_space`
87- Info must be a dictionary (can be empty)
89**Example:**
90```python
91def reset(self, seed=None, options=None):
92 super().reset(seed=seed)
94 # Initialize state
95 self.agent_pos = self.np_random.integers(0, self.grid_size, size=2)
96 self.goal_pos = self.np_random.integers(0, self.grid_size, size=2)
97 self.current_step = 0
99 observation = self._get_observation()
100 info = {"reset_complete": True} # "episode" is reserved for Monitor summaries
102 return observation, info
103```
105#### `step(self, action)`
107**Purpose:** Execute one timestep in the environment.
109**Requirements:**
110- Must return 5-tuple: `(observation, reward, terminated, truncated, info)`
111- Action must be valid according to `action_space`
112- Observation must match `observation_space`
113- Reward should be a float
114- Terminated: True if episode ended naturally (goal reached, failure, etc.)
115- Truncated: True if episode ended due to time limit
116- Info must be a dictionary
118**Example:**
119```python
120def step(self, action):
121 # Apply action
122 self.agent_pos += self._action_to_direction(action)
123 self.agent_pos = np.clip(self.agent_pos, 0, self.grid_size - 1)
124 self.current_step += 1
126 # Calculate reward
127 distance = np.linalg.norm(self.agent_pos - self.goal_pos)
128 goal_reached = distance < 1.0
130 if goal_reached:
131 reward = 100.0
132 else:
133 reward = float(-distance * 0.1)
135 # Check termination conditions
136 terminated = bool(goal_reached)
137 truncated = bool(self.current_step >= self.max_steps)
139 observation = self._get_observation()
140 info = {"distance": distance, "steps": self.current_step}
142 return observation, reward, terminated, truncated, info
143```

Method Details

__init__(self, ...)

Purpose: Initialize the environment and define spaces.

Requirements:

  • Must call super().__init__()
  • Must define self.action_space
  • Must define self.observation_space

Example:

def __init__(self, grid_size=10, max_steps=100):
    super().__init__()
    self.grid_size = grid_size
    self.max_steps = max_steps
    self.current_step = 0

    # Define spaces
    self.action_space = spaces.Discrete(4)
    self.observation_space = spaces.Box(
        low=0, high=grid_size-1, shape=(4,), dtype=np.float32
    )

reset(self, seed=None, options=None)

Purpose: Reset the environment to an initial state.

Requirements:

  • Must call super().reset(seed=seed)
  • Must return (observation, info) tuple
  • Observation must match observation_space
  • Info must be a dictionary (can be empty)

Example:

def reset(self, seed=None, options=None):
    super().reset(seed=seed)

    # Initialize state
    self.agent_pos = self.np_random.integers(0, self.grid_size, size=2)
    self.goal_pos = self.np_random.integers(0, self.grid_size, size=2)
    self.current_step = 0

    observation = self._get_observation()
    info = {"reset_complete": True}  # "episode" is reserved for Monitor summaries

    return observation, info

step(self, action)

Purpose: Execute one timestep in the environment.

Requirements:

  • Must return 5-tuple: (observation, reward, terminated, truncated, info)
  • Action must be valid according to action_space
  • Observation must match observation_space
  • Reward should be a float
  • Terminated: True if episode ended naturally (goal reached, failure, etc.)
  • Truncated: True if episode ended due to time limit
  • Info must be a dictionary

Example:

def step(self, action):
    # Apply action
    self.agent_pos += self._action_to_direction(action)
    self.agent_pos = np.clip(self.agent_pos, 0, self.grid_size - 1)
    self.current_step += 1

    # Calculate reward
    distance = np.linalg.norm(self.agent_pos - self.goal_pos)
    goal_reached = distance < 1.0

    if goal_reached:
        reward = 100.0
    else:
        reward = float(-distance * 0.1)

    # Check termination conditions
    terminated = bool(goal_reached)
    truncated = bool(self.current_step >= self.max_steps)

    observation = self._get_observation()
    info = {"distance": distance, "steps": self.current_step}

    return observation, reward, terminated, truncated, info
Raw lines 53 to 144
145## Space Types
147### Discrete
149For discrete actions (e.g., {0, 1, 2, 3}).
151```python
152self.action_space = spaces.Discrete(4) # 4 actions: 0, 1, 2, 3
153```
155**Important:** SB3 does NOT support `Discrete` spaces with `start != 0`. Always start from 0.

Discrete

For discrete actions (e.g., {0, 1, 2, 3}).

self.action_space = spaces.Discrete(4)  # 4 actions: 0, 1, 2, 3

Important: SB3 does NOT support Discrete spaces with start != 0. Always start from 0.

Raw lines 147 to 156
157### Box (Continuous)
159For continuous values within a range.
161```python
162# 1D continuous action in [-1, 1]
163self.action_space = spaces.Box(low=-1, high=1, shape=(1,), dtype=np.float32)
165# 2D position observation
166self.observation_space = spaces.Box(
167 low=0, high=10, shape=(2,), dtype=np.float32
170# 3D RGB image (channel-first format)
171self.observation_space = spaces.Box(
172 low=0, high=255, shape=(3, 84, 84), dtype=np.uint8
174```
176**Important for default CNN image preprocessing:**
177- Standard images are `dtype=np.uint8` in range [0, 255]
178- Use **channel-first** format: (channels, height, width)
179- SB3 automatically normalizes by dividing by 255
180- For pre-normalized float images, use CHW and `policy_kwargs={"normalize_images": False}`

Box (Continuous)

For continuous values within a range.

# 1D continuous action in [-1, 1]
self.action_space = spaces.Box(low=-1, high=1, shape=(1,), dtype=np.float32)

# 2D position observation
self.observation_space = spaces.Box(
    low=0, high=10, shape=(2,), dtype=np.float32
)

# 3D RGB image (channel-first format)
self.observation_space = spaces.Box(
    low=0, high=255, shape=(3, 84, 84), dtype=np.uint8
)

Important for default CNN image preprocessing:

  • Standard images are dtype=np.uint8 in range [0, 255]
  • Use channel-first format: (channels, height, width)
  • SB3 automatically normalizes by dividing by 255
  • For pre-normalized float images, use CHW and policy_kwargs={"normalize_images": False}
Raw lines 157 to 181
182### MultiDiscrete
184For multiple discrete variables.
186```python
187# Two discrete variables: first with 3 options, second with 4 options
188self.action_space = spaces.MultiDiscrete([3, 4])
189```
191**Important:** If your `MultiDiscrete` space uses multi-dimensional arrays (not a flat vector), the env checker will warn you. Use a wrapper to flatten the space or reshape observations/actions to match SB3's expected 1D layout. See the [SB3 custom env guide](https://stable-baselines3.readthedocs.io/en/v2.9.0/guide/custom_env.html) for the recommended wrapper pattern.

MultiDiscrete

For multiple discrete variables.

# Two discrete variables: first with 3 options, second with 4 options
self.action_space = spaces.MultiDiscrete([3, 4])

Important: If your MultiDiscrete space uses multi-dimensional arrays (not a flat vector), the env checker will warn you. Use a wrapper to flatten the space or reshape observations/actions to match SB3's expected 1D layout. See the SB3 custom env guide for the recommended wrapper pattern.

Raw lines 182 to 192
193### MultiBinary
195For binary vectors.
197```python
198# 5 binary flags
199self.action_space = spaces.MultiBinary(5) # e.g., [0, 1, 1, 0, 1]
200```

MultiBinary

For binary vectors.

# 5 binary flags
self.action_space = spaces.MultiBinary(5)  # e.g., [0, 1, 1, 0, 1]
Raw lines 193 to 201
202### Dict
204For dictionary observations (e.g., combining image with sensors).
206```python
207self.observation_space = spaces.Dict({
208 "image": spaces.Box(low=0, high=255, shape=(3, 64, 64), dtype=np.uint8),
209 "vector": spaces.Box(low=-10, high=10, shape=(4,), dtype=np.float32),
210 "discrete": spaces.Discrete(3),
211})
212```
214**Important:** When using Dict observations, use `"MultiInputPolicy"` instead of `"MlpPolicy"`.
216```python
217model = PPO("MultiInputPolicy", env, verbose=1)
218```

Dict

For dictionary observations (e.g., combining image with sensors).

self.observation_space = spaces.Dict({
    "image": spaces.Box(low=0, high=255, shape=(3, 64, 64), dtype=np.uint8),
    "vector": spaces.Box(low=-10, high=10, shape=(4,), dtype=np.float32),
    "discrete": spaces.Discrete(3),
})

Important: When using Dict observations, use "MultiInputPolicy" instead of "MlpPolicy".

model = PPO("MultiInputPolicy", env, verbose=1)
Raw lines 202 to 219
220### Tuple
222Tuple observations are not supported by SB3 policies. Convert to a single-level
223Dict or flat Box before training; a VecEnv accepting Tuple is not proof that an
224algorithm accepts it. The declaration below describes the unsupported input.
226```python
227self.observation_space = spaces.Tuple((
228 spaces.Box(low=0, high=1, shape=(4,), dtype=np.float32),
229 spaces.Discrete(3),
230))
231```

Tuple

Tuple observations are not supported by SB3 policies. Convert to a single-level Dict or flat Box before training; a VecEnv accepting Tuple is not proof that an algorithm accepts it. The declaration below describes the unsupported input.

self.observation_space = spaces.Tuple((
    spaces.Box(low=0, high=1, shape=(4,), dtype=np.float32),
    spaces.Discrete(3),
))
Raw lines 220 to 232
233## Important Constraints and Best Practices

Important Constraints and Best Practices

Raw lines 233 to 234
235### Data Types
237- **Observations:** Use `np.float32` for continuous values
238- **Images:** Use `np.uint8` in range [0, 255]
239- **Rewards:** Return a Python float (cast NumPy scalars explicitly)
240- **Terminated/Truncated:** Return Python bool

Data Types

  • Observations: Use np.float32 for continuous values
  • Images: Use np.uint8 in range [0, 255]
  • Rewards: Return a Python float (cast NumPy scalars explicitly)
  • Terminated/Truncated: Return Python bool
Raw lines 235 to 241
242### Random Number Generation
244Always use `self.np_random` for reproducibility:
246```python
247def reset(self, seed=None, options=None):
248 super().reset(seed=seed)
249 # Use self.np_random instead of np.random
250 random_pos = self.np_random.integers(0, 10, size=2)
251 random_float = self.np_random.random()
252```

Random Number Generation

Always use self.np_random for reproducibility:

def reset(self, seed=None, options=None):
    super().reset(seed=seed)
    # Use self.np_random instead of np.random
    random_pos = self.np_random.integers(0, 10, size=2)
    random_float = self.np_random.random()
Raw lines 242 to 253
254### Episode Termination
256- **Terminated:** Natural ending (goal reached, agent died, etc.)
257- **Truncated:** Artificial ending (time limit, external interrupt)
258- Gymnasium permits both flags True. SB3 records `TimeLimit.truncated = truncated and not terminated`; bootstrap only through pure truncation using the final observation.
259- Include relevant task state in observations; an intrinsic finite horizon also requires remaining time for a Markov model. Use TimeLimit for an external rollout cutoff.
261```python
262def step(self, action):
263 # ... environment logic ...
265 goal_reached = self._check_goal()
266 time_limit_exceeded = self.current_step >= self.max_steps
268 terminated = bool(goal_reached) # Natural ending
269 truncated = bool(time_limit_exceeded) # Time limit
271 return observation, reward, terminated, truncated, info
272```

Episode Termination

  • Terminated: Natural ending (goal reached, agent died, etc.)
  • Truncated: Artificial ending (time limit, external interrupt)
  • Gymnasium permits both flags True. SB3 records TimeLimit.truncated = truncated and not terminated; bootstrap only through pure truncation using the final observation.
  • Include relevant task state in observations; an intrinsic finite horizon also requires remaining time for a Markov model. Use TimeLimit for an external rollout cutoff.
def step(self, action):
    # ... environment logic ...

    goal_reached = self._check_goal()
    time_limit_exceeded = self.current_step >= self.max_steps

    terminated = bool(goal_reached)  # Natural ending
    truncated = bool(time_limit_exceeded)  # Time limit

    return observation, reward, terminated, truncated, info
Raw lines 254 to 273
274### Info Dictionary
276Use the info dict for debugging and logging:
278```python
279info = {
280 "episode_length": self.current_step,
281 "distance_to_goal": distance,
282 "success": goal_reached,
283 "total_reward": self.cumulative_reward,
285```
287**Special Keys:**
288- `"terminal_observation"`: Automatically added by VecEnv when episode ends

Info Dictionary

Use the info dict for debugging and logging:

info = {
    "episode_length": self.current_step,
    "distance_to_goal": distance,
    "success": goal_reached,
    "total_reward": self.cumulative_reward,
}

Special Keys:

  • "terminal_observation": Automatically added by VecEnv when episode ends
Raw lines 274 to 289
290## Advanced Features

Advanced Features

Raw lines 290 to 291
292### Metadata
294Provide rendering information:
296```python
297class CustomEnv(gym.Env):
298 metadata = {
299 "render_modes": ["human", "rgb_array"],
300 "render_fps": 30,
301 }
303 def __init__(self, render_mode=None):
304 super().__init__()
305 self.render_mode = render_mode
306 # ...
307```

Metadata

Provide rendering information:

class CustomEnv(gym.Env):
    metadata = {
        "render_modes": ["human", "rgb_array"],
        "render_fps": 30,
    }

    def __init__(self, render_mode=None):
        super().__init__()
        self.render_mode = render_mode
        # ...
Raw lines 292 to 308
309### Render Modes
311```python
312def render(self):
313 if self.render_mode == "human":
314 # Print or display for human viewing
315 print(f"Agent at {self.agent_pos}")
317 elif self.render_mode == "rgb_array":
318 # Return numpy array (height, width, 3) for video recording
319 canvas = np.zeros((500, 500, 3), dtype=np.uint8)
320 # Draw environment on canvas
321 return canvas
322```

Render Modes

def render(self):
    if self.render_mode == "human":
        # Print or display for human viewing
        print(f"Agent at {self.agent_pos}")

    elif self.render_mode == "rgb_array":
        # Return numpy array (height, width, 3) for video recording
        canvas = np.zeros((500, 500, 3), dtype=np.uint8)
        # Draw environment on canvas
        return canvas
Raw lines 309 to 323
324### Goal-Conditioned Environments (for HER)
326For Hindsight Experience Replay, use specific observation structure:
328```python
329self.observation_space = spaces.Dict({
330 "observation": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
331 "achieved_goal": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
332 "desired_goal": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
333})
335def compute_reward(self, achieved_goal, desired_goal, info):
336 """Required for HER environments"""
337 distance = np.linalg.norm(np.asarray(achieved_goal) - np.asarray(desired_goal), axis=-1)
338 return -distance
339```
341`compute_reward` must accept both one goal vector and a batch; `axis=-1`
342preserves the batch dimension. Its single-transition reward must match `step()`.
343Do not invent a `compute_reward` method on a non-goal environment: SB3 uses that
344method to detect goal environments.

Goal-Conditioned Environments (for HER)

For Hindsight Experience Replay, use specific observation structure:

self.observation_space = spaces.Dict({
    "observation": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
    "achieved_goal": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
    "desired_goal": spaces.Box(low=-10, high=10, shape=(3,), dtype=np.float32),
})

def compute_reward(self, achieved_goal, desired_goal, info):
    """Required for HER environments"""
    distance = np.linalg.norm(np.asarray(achieved_goal) - np.asarray(desired_goal), axis=-1)
    return -distance

compute_reward must accept both one goal vector and a batch; axis=-1 preserves the batch dimension. Its single-transition reward must match step(). Do not invent a compute_reward method on a non-goal environment: SB3 uses that method to detect goal environments.

Raw lines 324 to 345
346## Environment Validation
348Always validate your environment before training:
350```python
351from stable_baselines3.common.env_checker import check_env
353env = CustomEnv()
354check_env(env, warn=True)
355```
357**Common Validation Errors:**
3591. **"Observation is not within bounds"**
360 - Check that observations stay within defined space
361 - Ensure correct dtype (np.float32 for Box spaces)
3632. **"Reset should return tuple"**
364 - Return `(observation, info)`, not just observation
3663. **"Step should return 5-tuple"**
367 - Return `(obs, reward, terminated, truncated, info)`
3694. **"Action is out of bounds"**
370 - Verify action_space definition matches expected actions
3725. **"Observation/Action dtype mismatch"**
373 - Ensure observations match space dtype (usually np.float32)

Environment Validation

Always validate your environment before training:

from stable_baselines3.common.env_checker import check_env

env = CustomEnv()
check_env(env, warn=True)

Common Validation Errors:

  1. "Observation is not within bounds"
  2. Check that observations stay within defined space
  3. Ensure correct dtype (np.float32 for Box spaces)
  1. "Reset should return tuple"
  2. Return (observation, info), not just observation
  1. "Step should return 5-tuple"
  2. Return (obs, reward, terminated, truncated, info)
  1. "Action is out of bounds"
  2. Verify action_space definition matches expected actions
  1. "Observation/Action dtype mismatch"
  2. Ensure observations match space dtype (usually np.float32)
Raw lines 346 to 374
375## Environment Registration
377Register your environment with Gymnasium:
379```python
380import gymnasium as gym
381from gymnasium.envs.registration import register
383register(
384 id="MyCustomEnv-v0",
385 entry_point="my_module:CustomEnv",
386 max_episode_steps=200,
387 kwargs={"grid_size": 10}, # Default kwargs
390# Now can use with gym.make
391env = gym.make("MyCustomEnv-v0")
392```

Environment Registration

Register your environment with Gymnasium:

import gymnasium as gym
from gymnasium.envs.registration import register

register(
    id="MyCustomEnv-v0",
    entry_point="my_module:CustomEnv",
    max_episode_steps=200,
    kwargs={"grid_size": 10},  # Default kwargs
)

# Now can use with gym.make
env = gym.make("MyCustomEnv-v0")
Raw lines 375 to 393
394## Testing Custom Environments

Testing Custom Environments

Raw lines 394 to 395
396### Basic Testing
398```python
399def test_environment(env, n_episodes=5):
400 """Test environment with random actions"""
401 for episode in range(n_episodes):
402 obs, info = env.reset()
403 episode_reward = 0
404 done = False
405 steps = 0
407 while not done:
408 action = env.action_space.sample()
409 obs, reward, terminated, truncated, info = env.step(action)
410 episode_reward += reward
411 steps += 1
412 done = terminated or truncated
414 print(f"Episode {episode+1}: Reward={episode_reward:.2f}, Steps={steps}")
415```

Basic Testing

def test_environment(env, n_episodes=5):
    """Test environment with random actions"""
    for episode in range(n_episodes):
        obs, info = env.reset()
        episode_reward = 0
        done = False
        steps = 0

        while not done:
            action = env.action_space.sample()
            obs, reward, terminated, truncated, info = env.step(action)
            episode_reward += reward
            steps += 1
            done = terminated or truncated

        print(f"Episode {episode+1}: Reward={episode_reward:.2f}, Steps={steps}")
Raw lines 396 to 416
417### Training Test
419```python
420from stable_baselines3 import PPO
422def train_test(env, timesteps=10000):
423 """Quick training test"""
424 model = PPO("MlpPolicy", env, verbose=1)
425 model.learn(total_timesteps=timesteps)
427 # Evaluate
428 obs, info = env.reset()
429 for _ in range(100):
430 action, _states = model.predict(obs, deterministic=True)
431 obs, reward, terminated, truncated, info = env.step(action)
432 if terminated or truncated:
433 break
434```

Training Test

from stable_baselines3 import PPO

def train_test(env, timesteps=10000):
    """Quick training test"""
    model = PPO("MlpPolicy", env, verbose=1)
    model.learn(total_timesteps=timesteps)

    # Evaluate
    obs, info = env.reset()
    for _ in range(100):
        action, _states = model.predict(obs, deterministic=True)
        obs, reward, terminated, truncated, info = env.step(action)
        if terminated or truncated:
            break
Raw lines 417 to 435
436## Common Patterns

Common Patterns

Raw lines 436 to 437
438### Grid World
440```python
441class GridWorldEnv(gym.Env):
442 def __init__(self, size=10):
443 super().__init__()
444 self.size = size
445 self.action_space = spaces.Discrete(4) # up, down, left, right
446 self.observation_space = spaces.Box(0, size-1, shape=(4,), dtype=np.float32)
447 # _get_obs() concatenates agent and goal coordinates.
448```

Grid World

class GridWorldEnv(gym.Env):
    def __init__(self, size=10):
        super().__init__()
        self.size = size
        self.action_space = spaces.Discrete(4)  # up, down, left, right
        self.observation_space = spaces.Box(0, size-1, shape=(4,), dtype=np.float32)
        # _get_obs() concatenates agent and goal coordinates.
Raw lines 438 to 449
450### Continuous Control
452```python
453class ContinuousEnv(gym.Env):
454 def __init__(self):
455 super().__init__()
456 self.action_space = spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32)
457 self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=(8,), dtype=np.float32)
458```

Continuous Control

class ContinuousEnv(gym.Env):
    def __init__(self):
        super().__init__()
        self.action_space = spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32)
        self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=(8,), dtype=np.float32)
Raw lines 450 to 459
460### Image-Based Environment
462```python
463class VisionEnv(gym.Env):
464 def __init__(self):
465 super().__init__()
466 self.action_space = spaces.Discrete(4)
467 # Channel-first: (channels, height, width)
468 self.observation_space = spaces.Box(
469 low=0, high=255, shape=(3, 84, 84), dtype=np.uint8
470 )
471```

Image-Based Environment

class VisionEnv(gym.Env):
    def __init__(self):
        super().__init__()
        self.action_space = spaces.Discrete(4)
        # Channel-first: (channels, height, width)
        self.observation_space = spaces.Box(
            low=0, high=255, shape=(3, 84, 84), dtype=np.uint8
        )
Raw lines 460 to 472
473### Multi-Modal Environment
475```python
476class MultiModalEnv(gym.Env):
477 def __init__(self):
478 super().__init__()
479 self.action_space = spaces.Discrete(4)
480 self.observation_space = spaces.Dict({
481 "image": spaces.Box(0, 255, shape=(3, 64, 64), dtype=np.uint8),
482 "sensors": spaces.Box(-10, 10, shape=(4,), dtype=np.float32),
483 })
484```

Multi-Modal Environment

class MultiModalEnv(gym.Env):
    def __init__(self):
        super().__init__()
        self.action_space = spaces.Discrete(4)
        self.observation_space = spaces.Dict({
            "image": spaces.Box(0, 255, shape=(3, 64, 64), dtype=np.uint8),
            "sensors": spaces.Box(-10, 10, shape=(4,), dtype=np.float32),
        })
Raw lines 473 to 485
486## Performance Considerations

Performance Considerations

Raw lines 486 to 487
488### Efficient Observation Generation
490```python
491# Pre-allocate arrays
492def __init__(self):
493 # ...
494 self._obs_buffer = np.zeros(self.observation_space.shape, dtype=np.float32)
496def _get_observation(self):
497 # Reuse internal storage, but return an independent snapshot
498 self._obs_buffer[0] = self.agent_x
499 self._obs_buffer[1] = self.agent_y
500 return self._obs_buffer.copy() # Prevent later steps mutating an earlier observation
501```

Efficient Observation Generation

# Pre-allocate arrays
def __init__(self):
    # ...
    self._obs_buffer = np.zeros(self.observation_space.shape, dtype=np.float32)

def _get_observation(self):
    # Reuse internal storage, but return an independent snapshot
    self._obs_buffer[0] = self.agent_x
    self._obs_buffer[1] = self.agent_y
    return self._obs_buffer.copy()  # Prevent later steps mutating an earlier observation
Raw lines 488 to 502
503### Vectorization
505Make environment operations vectorizable:
507```python
508# Good: Uses numpy operations
509def step(self, action):
510 direction = np.array([[0,1], [0,-1], [1,0], [-1,0]])[action]
511 self.pos = np.clip(self.pos + direction, 0, self.size-1)
513# Avoid: Python loops when possible
514# for i in range(len(self.agents)):
515# self.agents[i].update()
516```

Vectorization

Make environment operations vectorizable:

# Good: Uses numpy operations
def step(self, action):
    direction = np.array([[0,1], [0,-1], [1,0], [-1,0]])[action]
    self.pos = np.clip(self.pos + direction, 0, self.size-1)

# Avoid: Python loops when possible
# for i in range(len(self.agents)):
#     self.agents[i].update()
Raw lines 503 to 517
518## Troubleshooting

Troubleshooting

Raw lines 518 to 519
520### "Observation out of bounds"
521- Check that all observations are within defined space
522- Verify correct dtype (np.float32 vs np.float64)

"Observation out of bounds"

  • Check that all observations are within defined space
  • Verify correct dtype (np.float32 vs np.float64)
Raw lines 520 to 523
524### "NaN or Inf in observation/reward"
525- Add checks: `assert np.isfinite(reward)`
526- Use `VecCheckNan` wrapper to catch issues

"NaN or Inf in observation/reward"

  • Add checks: assert np.isfinite(reward)
  • Use VecCheckNan wrapper to catch issues
Raw lines 524 to 527
528### "Policy doesn't learn"
529- Check reward scaling (normalize rewards)
530- Verify observation normalization
531- Ensure reward signal is meaningful
532- Check if exploration is sufficient

"Policy doesn't learn"

  • Check reward scaling (normalize rewards)
  • Verify observation normalization
  • Ensure reward signal is meaningful
  • Check if exploration is sufficient
Raw lines 528 to 533
534### "Training crashes"
535- Validate environment with `check_env()`
536- Check for race conditions in custom env
537- Verify action/observation spaces are consistent

"Training crashes"

  • Validate environment with check_env()
  • Check for race conditions in custom env
  • Verify action/observation spaces are consistent
Raw lines 534 to 538
539## Additional Resources
541- Template: [custom_env_template.py](../scripts/custom_env_template.py)
542- Gymnasium Documentation: https://gymnasium.farama.org/
543- SB3 Custom Env Guide: https://stable-baselines3.readthedocs.io/en/v2.9.0/guide/custom_env.html

Additional Resources

  • Template: custom_env_template.py
  • Gymnasium Documentation: https://gymnasium.farama.org/
  • SB3 Custom Env Guide: https://stable-baselines3.readthedocs.io/en/v2.9.0/guide/custom_env.html
Raw lines 539 to 543
Runs code scripts/custom_env_template.py :541