Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .actrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--secret-file .secrets
-P ubuntu-latest=catthehacker/ubuntu:act-latest
128 changes: 57 additions & 71 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
if: "!contains(github.event.head_commit.message, 'Bump version')"
strategy:
matrix:
python-version: [ '3.10' ]
python-version: [ '3.10', '3.11', '3.12' ]
os: [ 'ubuntu-latest', 'macos-latest' ]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -55,12 +55,11 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup Rye
id: setup-rye
uses: eifinger/setup-rye@v4
- name: Setup UV
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-prefix: ${{ matrix.os }}-latest-rye-test-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}
cache-suffix: ${{ matrix.os }}-${{ matrix.python-version }}
- name: actions/cache
uses: actions/cache@v3
with:
Expand All @@ -73,57 +72,57 @@ jobs:
run: |
set -x
sudo apt-get install -y xvfb libglu1-mesa x11-utils
rye pin --relaxed cpython@${{ matrix.python-version }}
rye sync --all-features
ROM_PASSWORD=${{ secrets.ROM_PASSWORD }} rye run import-roms
uv sync --all-extras
unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip
uv run python -m stable_retro.import "uncompressed ROMs"

- name: Install MacOS test and package dependencies
if: ${{ matrix.os == 'macos-latest' }}
- name: Verify ROM import success (Ubuntu)
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
set -x
brew install --cask xquartz
brew install swig libzip
# When building retro from source we may need the deprecated version of lua 5.1.
# brew install qt5 capnp
# Retro does not build in MacOS due to ancient requirements, so we will be installing retro==0.9.1 from pypi
# because it contains pre-build wheels for MacOS.
# chmod +x install-lua-macos.sh
# sudo ./install-lua-macos.sh
# echo 'export PATH="/opt/homebrew/opt/qt@5/bin:$PATH"' >> ~/.zshrc
# export SDKROOT=$(xcrun --sdk macosx --show-sdk-path)
# https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#adding-a-system-path
echo "/opt/X11/bin" >> $GITHUB_PATH
# https://github.com/ponty/PyVirtualDisplay/issues/42
if [ ! -d /tmp/.X11-unix ]; then
mkdir /tmp/.X11-unix
fi
sudo chmod 1777 /tmp/.X11-unix
sudo chown root /tmp/.X11-unix
rye pin --relaxed cpython@${{ matrix.python-version }}
rye sync --all-features
# Fix a bug in retro.data where it tries to load an inexistent version file
# sed -i '' 's/VERSION\.txt/VERSION/g' /Users/runner/work/plangym/plangym/.venv/lib/python3.10/site-packages/retro/__init__.py
if [ ! -d /Users/runner/work/plangym/plangym/.venv/lib/python3.10/site-packages/retro/VERSION ]; then
echo "0.9.1" > /Users/runner/work/plangym/plangym/.venv/lib/python3.10/site-packages/retro/VERSION.txt
fi
ROM_PASSWORD=${{ secrets.ROM_PASSWORD }} rye run import-roms
uv run python -c "import stable_retro; games = stable_retro.data.list_games(); print(f'Total games: {len(games)}'); assert len(games) > 60, 'ROM import failed'"

- name: Install MacOS test and package dependencies
if: ${{ matrix.os == 'macos-latest' }}
run: |
set -x
# XQuartz not needed - rendering tests are skipped on macOS (SKIP_RENDER=True)
# macOS doesn't support headless rendering (osmesa/egl) - only Linux does
brew install swig libzip
uv sync --all-extras
# Fix a bug in retro.data where it tries to load an inexistent version file
# Only applies to Python 3.10 where stable-retro is installed
RETRO_DIR="/Users/runner/work/plangym/plangym/.venv/lib/python${{ matrix.python-version }}/site-packages/retro"
if [ -d "$RETRO_DIR" ] && [ ! -f "$RETRO_DIR/VERSION.txt" ]; then
echo "0.9.1" > "$RETRO_DIR/VERSION.txt"
fi
- name: Import Retro ROMs (macOS Intel)
if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }}
run: |
set -x
unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip
uv run python -m stable_retro.import "uncompressed ROMs"

- name: Verify ROM import success (macOS Intel)
if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }}
run: |
uv run python -c "import stable_retro; games = stable_retro.data.list_games(); print(f'Total games: {len(games)}'); assert len(games) > 60, 'ROM import failed'"

- name: Run Pytest on MacOS
if: ${{ matrix.os == 'macos-latest' }}
run: |
set -x
# TODO: Figure out how to emulate a display in headless machines, and figure out why the commented files fail
# SKIP_RENDER=True rye run pytest tests/test_registry.py tests/videogames/test_retro.py
SKIP_RENDER=True rye run pytest tests/control tests/videogames/test_atari.py tests/videogames/test_nes.py tests/test_core.py tests/test_utils.py
# SKIP_RENDER=True uv run pytest tests/test_registry.py tests/videogames/test_retro.py
SKIP_RENDER=True uv run pytest tests/control tests/videogames/test_atari.py tests/videogames/test_nes.py tests/test_core.py tests/test_utils.py

- name: Run code coverage on Ubuntu
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
set -x
xvfb-run -s "-screen 0 1400x900x24" rye run codecov
xvfb-run -s "-screen 0 1400x900x24" make codecov

- name: Upload coverage report
# if: ${{ matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest' }}
# if: ${{ matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest' }}
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: codecov/codecov-action@v4
with:
Expand Down Expand Up @@ -161,12 +160,11 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Setup Rye
id: setup-rye
uses: eifinger/setup-rye@v4
- name: Setup UV
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-prefix: ubuntu-latest-rye-build-3.10-${{ hashFiles('pyproject.toml') }}
cache-suffix: ubuntu-build
- name: actions/cache
uses: actions/cache@v3
with:
Expand All @@ -176,9 +174,7 @@ jobs:
- name: Install build dependencies
run: |
set -x
pip install uv
rye install bump2version
rye install twine
uv tool install bump2version --force

- name: Create unique version for test.pypi
run: |
Expand All @@ -191,27 +187,24 @@ jobs:
- name: Build package
run: |
set -x
rye build --clean
twine check dist/*
uv build

- name: Publish package to TestPyPI
env:
TEST_PYPI_PASS: ${{ secrets.TEST_PYPI_PASS }}
if: "'$TEST_PYPI_PASS' != ''"
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_PASS }}
repository-url: https://test.pypi.org/legacy/
skip-existing: true
run: |
set -x
uv publish --publish-url https://test.pypi.org/legacy/ --token $TEST_PYPI_PASS || true

- name: Install dependencies
env:
UV_SYSTEM_PYTHON: 1
run: |
set -x
sudo apt-get install -y xvfb libglu1-mesa x11-utils
rye lock --all-features
uv pip install -r requirements.lock
uv lock
uv pip install -r pyproject.toml --all-extras
uv pip install dist/*.whl
# ROM_PASSWORD=${{ secrets.ROM_PASSWORD }} python -m plangym.scripts.import_retro_roms

Expand Down Expand Up @@ -270,28 +263,21 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Setup Rye
id: setup-rye
uses: eifinger/setup-rye@v4
- name: Setup UV
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-prefix: ubuntu-latest-rye-release-3.10-${{ hashFiles('pyproject.toml') }}
- name: Install dependencies
run: |
set -x
rye install twine
cache-suffix: ubuntu-release

- name: Build package
run: |
set -x
rye build --clean
twine check dist/*
uv build

- name: Publish package to PyPI
env:
PYPI_PASS: ${{ secrets.PYPI_PASS }}
if: "'$PYPI_PASS' != ''"
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_PASS }}
skip-existing: true
run: |
set -x
uv publish --token $PYPI_PASS
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ celerybeat-schedule
# dotenv
.env

# act secrets
.secrets

# virtualenv
.venv
venv/
Expand Down Expand Up @@ -133,3 +136,4 @@ outputs/

*.pck
*.npy
CLAUDE.md
97 changes: 97 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Plangym is a Python library that extends Gymnasium (OpenAI Gym) environments for planning algorithms. The key differentiator is the ability to get/set complete environment state, enabling deterministic rollouts from arbitrary states - critical for planning algorithms that need to branch execution.

## Common Commands

### Development
```bash
make style # Format code with ruff (auto-fix)
make check # Check code style without modifying
```

### Testing
```bash
# Full test suite
make test

# Individual test targets
make test-parallel # Run parallel tests (uses pytest-xdist with n workers)
make test-singlecore # Run classic control tests single-threaded
make test-doctest # Run doctests in source files

# Run a single test file
MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 uv run pytest tests/control/test_dm_control.py -s

# Run a specific test
uv run pytest tests/test_core.py::TestCoreEnv::test_step -v
```

### Documentation
```bash
make build-docs # Build sphinx documentation
make serve-docs # Serve docs locally
```

### Environment Variables
- `MUJOCO_GL=egl` - Required for MuJoCo rendering in headless environments
- `PYVIRTUALDISPLAY_DISPLAYFD=0` - Virtual display for rendering tests
- `SKIP_CLASSIC_CONTROL=1` - Skip classic control tests in parallel runs
- `n=2` - Number of parallel workers (default 2)

## Architecture

### Core Abstraction Hierarchy

```
PlanEnv (Abstract Base - src/plangym/core.py)
└── PlangymEnv (Gym Wrapper)
├── ClassicControl, Box2DEnv, DMControlEnv (control/)
└── VideogameEnv (videogames/)
├── AtariEnv, RetroEnv, MarioEnv
└── MontezumaEnv
```

### Key Classes

- **`PlanEnv`** (`core.py`): Abstract base defining the interface. Key methods: `get_state()`, `set_state()`, `apply_action()`, `apply_reset()`. Subclasses must implement these to enable planning.

- **`PlangymEnv`** (`core.py`): Wraps Gymnasium environments. Handles observation types (`coords`, `rgb`, `grayscale`), wrapper composition, and time limit removal.

- **`VectorizedEnv`** (`vectorization/env.py`): Base for parallel execution. Implementations: `ParallelEnv` (multiprocessing), `RayEnv` (distributed).

### Entry Point

The `make()` function in `registry.py` is the main factory. It routes to the correct environment class based on the environment name:
```python
import plangym
env = plangym.make(name="CartPole-v1") # Classic control
env = plangym.make(name="ALE/MsPacman-v5", n_workers=4) # Parallel Atari
```

### Design Patterns

1. **Delayed Setup**: `delay_setup=True` defers initialization, enabling serialization before setup (for distributed workers).

2. **Multi-step Execution**: `dt` parameter applies same action multiple times; combined with `frameskip` for total steps.

3. **Post-processing Hooks**: Override `process_obs()`, `process_reward()`, `process_terminal()`, `process_info()` in subclasses for custom transformations.

### Test Framework

Shared test base classes in `src/plangym/api_tests.py`:
- `TestPlanEnv`: Tests state management
- `TestPlangymEnv`: Tests Gym-wrapped environments
- `generate_test_cases()`: Parameterized tests across obs_types, render_modes, workers

## Source Layout

- `src/plangym/core.py` - Base classes (`PlanEnv`, `PlangymEnv`)
- `src/plangym/registry.py` - `make()` factory function
- `src/plangym/control/` - Physics-based environments (classic control, dm_control, mujoco)
- `src/plangym/videogames/` - Emulator-based environments (Atari, Retro, Mario)
- `src/plangym/vectorization/` - Parallel execution (`ParallelEnv`, `RayEnv`)
Loading
Loading