Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/envs/gym_env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: Gym Environment Server
emoji: 🎮
colorFrom: '#0E84B5'
colorTo: '#34D399'
sdk: docker
pinned: false
app_port: 8000
base_path: /web
tags:
- openenv
---

# Gym Environment

Integration of OpenAI Gym/Gymnasium environments with the OpenEnv framework. Gymnasium provides a wide variety of environments for reinforcement learning research and development.

## Supported Environments

Gymnasium includes numerous environments across different categories:

### Classic Control
- **CartPole** - Balance a pole on a moving cart
- **Pendulum** - Swing up and balance an inverted pendulum
- **Acrobot** - Swing up a two-link robotic arm
- **MountainCar** - Drive up a mountain with limited power

### Box2D
- **LunarLander** - Land a spacecraft safely
- **BipedalWalker** - Train a 2D biped to walk
- **CarRacing** - Race a car around a track

And many more! For a complete list, see [Gymnasium documentation](https://gymnasium.farama.org/environments/classic_control/).

## Architecture

```
┌────────────────────────────────────┐
│ RL Training Code (Client) │
│ GymEnv.step(action) │
└──────────────┬─────────────────────┘
│ HTTP
┌──────────────▼─────────────────────┐
│ FastAPI Server (Docker) │
│ GymEnvironment │
│ ├─ Wraps Gymnasium Env │
│ ├─ Handles observations │
│ └─ Action execution │
└────────────────────────────────────┘
```

## Installation & Usage

### Option 1: Local Development (without Docker)

**Requirements:**
- Python 3.11+
- gymnasium installed: `pip install gymnasium`

```python
# Connect to local server
from envs.gym_env import GymEnvironment, GymAction

# Start local server manually
# python -m envs.gym_env.server.app

env = GymEnvironment(base_url="http://0.0.0.0:8000")

# Reset environment
result = env.reset()
print(f"Observation : {result.observation.state}")
print(f"Action space: {result.observation.legal_actions}")

# Take actions
for _ in range(100):
action = 1 # Example action
result = env.step(GymAction(action=[action]))
print(f"Reward: {result.reward}, Done: {result.done}")
if result.done:
break

# Cleanup
env.close()

```

### Option 2: Docker (Recommended)

**Build Gym image:**

```bash
cd OpenEnv

# Build the image
docker build \
-f src/envs/gym_env/server/Dockerfile \
-t gym-env:latest \
.
```
12 changes: 12 additions & 0 deletions src/envs/gym_env/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Generic Gymnasium environment integration for OpenEnv."""

from .client import GymEnvironment
from .models import GymAction, GymObservation, GymState

__all__ = ["GymEnvironment", "GymAction", "GymObservation", "GymState"]
101 changes: 101 additions & 0 deletions src/envs/gym_env/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""HTTP client for generic Gymnasium environments served over HTTP."""

from __future__ import annotations

from typing import Any, Dict, TYPE_CHECKING

from core.client_types import StepResult

from core.http_env_client import HTTPEnvClient

from .models import GymAction, GymObservation, GymState

if TYPE_CHECKING:
from core.containers.runtime import ContainerProvider


class GymEnvironment(HTTPEnvClient[GymAction, GymObservation]):
"""Client for interacting with Gymnasium environments over HTTP.

Example:
>>> client = GymEnvironment(base_url="http://localhost:8000")
>>> result = client.reset()
>>> print(result.observation.state)
>>> result = client.step(GymAction(action=1))
>>> print(result.reward, result.done)

Example with Docker:
>>> client = GymEnvironment.from_docker_image("generic-gym-env:latest")
>>> _ = client.reset()
>>> _ = client.step(GymAction(action=0))
"""

def _step_payload(self, action: GymAction) -> Dict[str, Any]:
"""
Convert GymAction to JSON payload for step request.

Args:
action: GymAction instance.

Returns:
Dictionary representation suitable for JSON encoding.
"""
payload: Dict[str, Any] = {"action": action.action}
if action.metadata:
payload["metadata"] = action.metadata
return payload

def _parse_result(self, payload: Dict[str, Any]) -> StepResult[GymObservation]:
"""
Parse server response into StepResult[GymObservation].

Args:
payload: JSON response from server.

Returns:
StepResult with GymObservation.
"""
obs_data = payload.get("observation", {})

observation = GymObservation(
state=obs_data.get("state"),
legal_actions=obs_data.get("legal_actions"),
episode_length=obs_data.get("episode_length", 0),
total_reward=obs_data.get("total_reward", 0.0),
done=bool(payload.get("done", False)),
reward=payload.get("reward"),
metadata=obs_data.get("metadata", {}),
)

return StepResult(
observation=observation,
reward=payload.get("reward"),
done=bool(payload.get("done", False)),
)

def _parse_state(self, payload: Dict[str, Any]) -> GymState:
"""
Parse server response into GymState object.

Args:
payload: JSON response from /state endpoint.

Returns:
GymState object with environment state information.
"""
return GymState(
env_id=payload.get("env_id", "Unknown"),
episode_id=payload.get("episode_id"),
step_count=payload.get("step_count", 0),
render_mode=payload.get("render_mode"),
max_steps=payload.get("max_steps"),
seed=payload.get("seed"),
episode_length=payload.get("episode_length", 0),
total_reward=payload.get("total_reward", 0.0),
)
48 changes: 48 additions & 0 deletions src/envs/gym_env/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
Data models for Gymnasium-based environments.

This module defines generic Action, Observation, and State representations
used by the Gym environment integration.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Optional

from core.env_server import Action, Observation, State


@dataclass
class GymAction(Action):
"""Generic action wrapper for Gymnasium environments."""

action: Any


@dataclass
class GymObservation(Observation):
"""Observation returned by a Gymnasium environment."""

state: Any
legal_actions: Optional[Any] = None
episode_length: int = 0
total_reward: float = 0.0


@dataclass
class GymState(State):
"""Server-side state snapshot for Gymnasium environments."""

env_id: str = "Unknown"
render_mode: Optional[str] = None
max_steps: Optional[int] = None
seed: Optional[int] = None
episode_length: int = 0
total_reward: float = 0.0
51 changes: 51 additions & 0 deletions src/envs/gym_env/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Dockerfile for Atari Environment
# This image provides Atari 2600 games via the Arcade Learning Environment (ALE)

# Configurable base image - defaults to local build, can be overridden for CI/CD
# Base image provides: fastapi, uvicorn, requests, curl, PYTHONPATH=/app/src
#
# Local build: docker build -t envtorch-base:latest -f src/core/containers/images/Dockerfile .
# docker build -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
#
# CI/CD build: docker build --build-arg BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest \
# -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
ARG BASE_IMAGE=openenv-base:latest
FROM ${BASE_IMAGE}

# Install dependencies
COPY src/envs/gym_env/server/requirements.txt /tmp/requirements.txt
RUN apt-get update && apt-get install -y \
swig \
build-essential \
python3-dev

RUN pip install -r /tmp/requirements.txt && rm /tmp/requirements.txt

# Copy OpenEnv core (base image already set WORKDIR=/app)
COPY src/core/ /app/src/core/

# Copy Gym environment code
COPY src/envs/gym_env/ /app/src/envs/gym_env/

# Copy README for web interface documentation
COPY src/envs/gym_env/README.md /app/README.md

ARG GYM_ENVIRONMENT_ID="MountainCarContinuous-v0"
ARG GYM_RENDER_MODE="rgb_array"
ENV ADDITIONAL_PARAMETERS_YAML_FILE=""


# --- Runtime environment with defaults ---
# These ENV lines set defaults but still allow runtime overrides
ENV GYM_ENVIRONMENT_ID=${GYM_ENVIRONMENT_ID}
ENV ADDITIONAL_PARAMETERS=${ADDITIONAL_PARAMETERS_YAML_FILE}

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

# Run the FastAPI server
CMD ["uvicorn", "envs.gym_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file.
Empty file.
55 changes: 55 additions & 0 deletions src/envs/gym_env/server/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""FastAPI application that exposes a generic Gymnasium environment."""

import os

from core.env_server import create_app

from ..models import GymAction, GymObservation
from .gymnasium_environment import GymnasiumEnvironment
import yaml

# Environment configuration via environment variables
env_id = os.getenv("GYM_ENVIRONMENT_ID", "MountainCarContinuous-v0")
render_mode = os.getenv("GYM_RENDER_MODE", "rgb_array")

max_steps_str = os.getenv("GYM_MAX_STEPS")
max_steps = int(max_steps_str) if max_steps_str else 1000

seed_str = os.getenv("GYM_SEED")
seed = int(seed_str) if seed_str else None
yaml_param_file_path = os.getenv("ADDITIONAL_PARAMETERS_YAML_FILE")
additional_params = {}

# Load additional parameters from YAML if file path is provided
if yaml_param_file_path and os.path.exists(yaml_param_file_path):
with open(yaml_param_file_path, "r") as f:
additional_params = yaml.safe_load(f)

# Create the environment instance
env = GymnasiumEnvironment(
env_id=env_id,
render_mode=render_mode,
max_steps=max_steps,
seed=seed,
**additional_params,
)

# Create the FastAPI app with web interface and README integration
app = create_app(
env,
GymAction,
GymObservation,
env_name=env_id.lower().replace("-", "_"),
)


if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8010)
Loading
Loading