-
Notifications
You must be signed in to change notification settings - Fork 445
Add generic Gymnasium environment #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5dc124d
8624168
af00607
53d6500
3a4bf81
ff1d0fa
fe3c0cd
acc24d7
538274a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 \ | ||
| . | ||
| ``` |
| 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"] |
| 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), | ||
| ) |
| 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 |
| 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 . | ||||||||||||||||||||||
|
Comment on lines
+7
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Example commands in comments still reference
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/envs/gym_env/server/Dockerfile
Line: 7:11
Comment:
**style:** Example commands in comments still reference `atari_env` instead of `gym_env`.
```suggestion
# Local build: docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
# docker build -f src/envs/gym_env/server/Dockerfile -t gym-env:latest .
#
# CI/CD build: docker build --build-arg BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest \
# -f src/envs/gym_env/server/Dockerfile -t gym-env:latest .
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||
| 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} | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: ENV variable name mismatch - setting
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/envs/gym_env/server/Dockerfile
Line: 41:41
Comment:
**logic:** ENV variable name mismatch - setting `ADDITIONAL_PARAMETERS` but the code expects `ADDITIONAL_PARAMETERS_YAML_FILE`.
```suggestion
ENV ADDITIONAL_PARAMETERS_YAML_FILE=${ADDITIONAL_PARAMETERS_YAML_FILE}
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # 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"] | ||||||||||||||||||||||
| 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) | ||||||||||||||||
|
Comment on lines
+30
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic:
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/envs/gym_env/server/app.py
Line: 30:32
Comment:
**logic:** `yaml.safe_load()` returns `None` for empty YAML files, which will cause `**additional_params` to fail on line 40.
```suggestion
if yaml_param_file_path and os.path.exists(yaml_param_file_path):
with open(yaml_param_file_path, "r") as f:
loaded_params = yaml.safe_load(f)
additional_params = loaded_params if loaded_params else {}
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||
|
|
||||||||||||||||
| # 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) | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Header comments still reference Atari instead of Gym/Gymnasium.
Prompt To Fix With AI