Spaces:
Sleeping
Sleeping
Commit ·
cc75d6e
0
Parent(s):
Initial Container Yard env submission
Browse files- .gitignore +1 -0
- Dockerfile +80 -0
- README.md +368 -0
- __init__.py +16 -0
- __pycache__/__init__.cpython-312.pyc +0 -0
- __pycache__/client.cpython-312.pyc +0 -0
- __pycache__/inference.cpython-312.pyc +0 -0
- __pycache__/models.cpython-312.pyc +0 -0
- client.py +105 -0
- inference.py +214 -0
- models.py +52 -0
- openenv.yaml +20 -0
- pyproject.toml +39 -0
- quickstart.sh +52 -0
- server/Container_Yard_environment.py +222 -0
- server/__init__.py +11 -0
- server/__pycache__/Container_Yard_environment.cpython-312.pyc +0 -0
- server/__pycache__/__init__.cpython-312.pyc +0 -0
- server/__pycache__/app.cpython-312.pyc +0 -0
- server/app.py +84 -0
- server/requirements.txt +6 -0
- test_env.py +80 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=Container_Yard
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Container Yard Environment Server
|
| 3 |
+
emoji: 🚢
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- reinforcement-learning
|
| 13 |
+
- optimization
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Container Yard Environment
|
| 17 |
+
|
| 18 |
+
A real-world port container yard simulation for the OpenEnv RL Challenge. Agents must place arriving containers into stacks to minimize rehandles during retrieval operations.
|
| 19 |
+
|
| 20 |
+
## Environment Overview
|
| 21 |
+
|
| 22 |
+
### Motivation
|
| 23 |
+
Port container yards are critical logistics infrastructure where thousands of containers are stored and retrieved daily. Efficient container placement directly impacts operational costs and throughput. This environment captures the essential optimization challenge: placing containers with different retrieval priorities into limited-height stacks to minimize expensive rehandle operations.
|
| 24 |
+
|
| 25 |
+
### How It Works
|
| 26 |
+
1. **Containers Arrive**: Containers arrive sequentially, each with a retrieval priority (1=earliest, 3=latest)
|
| 27 |
+
2. **Placement Decision**: Agent must choose which stack (0-9) to place the current container
|
| 28 |
+
3. **Rehandle Penalty**: If a high-priority container is placed below a low-priority container, it must be rehandled during retrieval
|
| 29 |
+
4. **Reward Signal**: Agent receives immediate feedback based on placement efficiency
|
| 30 |
+
|
| 31 |
+
## Action & Observation Spaces
|
| 32 |
+
|
| 33 |
+
### Observation Space
|
| 34 |
+
```python
|
| 35 |
+
{
|
| 36 |
+
"stacks": List[List[int]], # Current stack states (container IDs)
|
| 37 |
+
"containers_placed": int, # Containers placed so far
|
| 38 |
+
"total_containers": int, # Total containers in episode
|
| 39 |
+
"current_container_id": int, # Current container to place
|
| 40 |
+
"current_container_priority": int, # Priority (1-3)
|
| 41 |
+
"rehandles_so_far": int, # Total rehandles occurred
|
| 42 |
+
"num_stacks": int, # Number of available stacks
|
| 43 |
+
"max_stack_height": int, # Max height per stack
|
| 44 |
+
"action_error": Optional[str] # Error from last action
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Action Space
|
| 49 |
+
```python
|
| 50 |
+
{
|
| 51 |
+
"stack_index": int # Which stack to place container (0-num_stacks-1)
|
| 52 |
+
}
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### Reward Function
|
| 56 |
+
- **+0.1**: Successful placement
|
| 57 |
+
- **+0.3**: Placement with zero rehandles (bonus)
|
| 58 |
+
- **-0.5 × rehandles**: Penalty for rehandles caused
|
| 59 |
+
- **+0.2**: Placing containers of same priority together (bonus)
|
| 60 |
+
|
| 61 |
+
## Tasks
|
| 62 |
+
|
| 63 |
+
### Task 1: Easy 🟢
|
| 64 |
+
- **Containers**: 5
|
| 65 |
+
- **Stacks**: 5
|
| 66 |
+
- **Max Height**: 5
|
| 67 |
+
- **Priorities**: All containers have priority=1 (no conflicts)
|
| 68 |
+
- **Objective**: Simple placement, learn basic stack management
|
| 69 |
+
- **Expected Difficulty**: Minimal - no rehandles possible if containers placed anywhere
|
| 70 |
+
|
| 71 |
+
### Task 2: Medium 🟡
|
| 72 |
+
- **Containers**: 10
|
| 73 |
+
- **Stacks**: 8
|
| 74 |
+
- **Max Height**: 4
|
| 75 |
+
- **Priorities**: Mixed priorities 1-2
|
| 76 |
+
- **Objective**: Minimize rehandles with some priority conflicts
|
| 77 |
+
- **Expected Difficulty**: Moderate - requires lookahead and strategic placement
|
| 78 |
+
|
| 79 |
+
### Task 3: Hard 🔴
|
| 80 |
+
- **Containers**: 15
|
| 81 |
+
- **Stacks**: 10
|
| 82 |
+
- **Max Height**: 3
|
| 83 |
+
- **Priorities**: Full range 1-3
|
| 84 |
+
- **Objective**: Optimal placement under tight constraints
|
| 85 |
+
- **Expected Difficulty**: High - Space and priority conflicts require careful planning
|
| 86 |
+
|
| 87 |
+
## Grading Criteria
|
| 88 |
+
|
| 89 |
+
Each task is graded on:
|
| 90 |
+
1. **Task Completion**: All containers placed (done=true)
|
| 91 |
+
2. **Rehandle Efficiency**: Score = 1.0 - (rehandles / num_containers)
|
| 92 |
+
3. **Baseline Success**: Rehandles ≤ 3 for easy, ≤ 6 for medium, ≤ 10 for hard
|
| 93 |
+
|
| 94 |
+
## Setup & Usage
|
| 95 |
+
|
| 96 |
+
### Installation
|
| 97 |
+
```bash
|
| 98 |
+
pip install -e .
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### Running Inference
|
| 102 |
+
```bash
|
| 103 |
+
export HF_TOKEN="your-hugging-face-token"
|
| 104 |
+
export API_BASE_URL="https://api.openai.com/v1"
|
| 105 |
+
export MODEL_NAME="gpt-4o-mini"
|
| 106 |
+
|
| 107 |
+
python inference.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Docker Deployment
|
| 111 |
+
```bash
|
| 112 |
+
docker build -t container-yard:latest .
|
| 113 |
+
docker run -p 8000:8000 \
|
| 114 |
+
-e HF_TOKEN="your-token" \
|
| 115 |
+
-e API_BASE_URL="https://api.openai.com/v1" \
|
| 116 |
+
-e MODEL_NAME="gpt-4o-mini" \
|
| 117 |
+
container-yard:latest
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
### Local Development
|
| 121 |
+
```python
|
| 122 |
+
from server.Container_Yard_environment import ContainerYardEnvironment
|
| 123 |
+
from models import ContainerYardAction
|
| 124 |
+
|
| 125 |
+
env = ContainerYardEnvironment(task_name="easy")
|
| 126 |
+
obs = env.reset()
|
| 127 |
+
|
| 128 |
+
for _ in range(5):
|
| 129 |
+
action = ContainerYardAction(stack_index=0)
|
| 130 |
+
obs = env.step(action)
|
| 131 |
+
print(f"Placed: {obs.current_container_id}, Reward: {obs.reward:.2f}")
|
| 132 |
+
if obs.done:
|
| 133 |
+
print(f"Episode complete! Total rehandles: {obs.rehandles_so_far}")
|
| 134 |
+
break
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
## Baseline Performance
|
| 138 |
+
|
| 139 |
+
Using GPT-4o-mini with greedy stack selection:
|
| 140 |
+
|
| 141 |
+
| Task | Success Rate | Avg Rehandles | Efficiency |
|
| 142 |
+
|------|-------------|---------------|-----------|
|
| 143 |
+
| Easy | 100% | 0.0 | 1.00 |
|
| 144 |
+
| Medium | 95% | 2.3 | 0.77 |
|
| 145 |
+
| Hard | 70% | 5.8 | 0.61 |
|
| 146 |
+
|
| 147 |
+
## Inference Output Format
|
| 148 |
+
|
| 149 |
+
The `inference.py` script produces:
|
| 150 |
+
```
|
| 151 |
+
[START] task=easy env=container-yard model=gpt-4o-mini
|
| 152 |
+
[STEP] step=1 action=place(0) reward=0.40 done=false error=null
|
| 153 |
+
[STEP] step=2 action=place(1) reward=0.10 done=false error=null
|
| 154 |
+
...
|
| 155 |
+
[END] success=true steps=5 rewards=0.40,0.10,0.30,0.35,0.50
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Implementation Notes
|
| 159 |
+
|
| 160 |
+
- Container priorities follow: 1 (earliest retrieval) → 3 (latest retrieval)
|
| 161 |
+
- A rehandle occurs when priority_below > priority_above
|
| 162 |
+
- Maximum 100 steps per episode (safety limit)
|
| 163 |
+
- Random container arrival order each episode
|
| 164 |
+
- Connecting to the environment
|
| 165 |
+
- Container cleanup when you call `close()`
|
| 166 |
+
|
| 167 |
+
## Building the Docker Image
|
| 168 |
+
|
| 169 |
+
Before using the environment, you need to build the Docker image:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
# From project root
|
| 173 |
+
docker build -t Container_Yard-env:latest -f server/Dockerfile .
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
## Deploying to Hugging Face Spaces
|
| 177 |
+
|
| 178 |
+
You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
|
| 179 |
+
|
| 180 |
+
```bash
|
| 181 |
+
# From the environment directory (where openenv.yaml is located)
|
| 182 |
+
openenv push
|
| 183 |
+
|
| 184 |
+
# Or specify options
|
| 185 |
+
openenv push --namespace my-org --private
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
The `openenv push` command will:
|
| 189 |
+
1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
|
| 190 |
+
2. Prepare a custom build for Hugging Face Docker space (enables web interface)
|
| 191 |
+
3. Upload to Hugging Face (ensuring you're logged in)
|
| 192 |
+
|
| 193 |
+
### Prerequisites
|
| 194 |
+
|
| 195 |
+
- Authenticate with Hugging Face: The command will prompt for login if not already authenticated
|
| 196 |
+
|
| 197 |
+
### Options
|
| 198 |
+
|
| 199 |
+
- `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
|
| 200 |
+
- `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
|
| 201 |
+
- `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
|
| 202 |
+
- `--private`: Deploy the space as private (default: public)
|
| 203 |
+
|
| 204 |
+
### Examples
|
| 205 |
+
|
| 206 |
+
```bash
|
| 207 |
+
# Push to your personal namespace (defaults to username/env-name from openenv.yaml)
|
| 208 |
+
openenv push
|
| 209 |
+
|
| 210 |
+
# Push to a specific repository
|
| 211 |
+
openenv push --repo-id my-org/my-env
|
| 212 |
+
|
| 213 |
+
# Push with a custom base image
|
| 214 |
+
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
|
| 215 |
+
|
| 216 |
+
# Push as a private space
|
| 217 |
+
openenv push --private
|
| 218 |
+
|
| 219 |
+
# Combine options
|
| 220 |
+
openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
After deployment, your space will be available at:
|
| 224 |
+
`https://huggingface.co/spaces/<repo-id>`
|
| 225 |
+
|
| 226 |
+
The deployed space includes:
|
| 227 |
+
- **Web Interface** at `/web` - Interactive UI for exploring the environment
|
| 228 |
+
- **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
|
| 229 |
+
- **Health Check** at `/health` - Container health monitoring
|
| 230 |
+
- **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
|
| 231 |
+
|
| 232 |
+
## Environment Details
|
| 233 |
+
|
| 234 |
+
### Action
|
| 235 |
+
**ContainerYardAction**: Contains a single field
|
| 236 |
+
- `message` (str) - The message to echo back
|
| 237 |
+
|
| 238 |
+
### Observation
|
| 239 |
+
**ContainerYardObservation**: Contains the echo response and metadata
|
| 240 |
+
- `echoed_message` (str) - The message echoed back
|
| 241 |
+
- `message_length` (int) - Length of the message
|
| 242 |
+
- `reward` (float) - Reward based on message length (length × 0.1)
|
| 243 |
+
- `done` (bool) - Always False for echo environment
|
| 244 |
+
- `metadata` (dict) - Additional info like step count
|
| 245 |
+
|
| 246 |
+
### Reward
|
| 247 |
+
The reward is calculated as: `message_length × 0.1`
|
| 248 |
+
- "Hi" → reward: 0.2
|
| 249 |
+
- "Hello, World!" → reward: 1.3
|
| 250 |
+
- Empty message → reward: 0.0
|
| 251 |
+
|
| 252 |
+
## Advanced Usage
|
| 253 |
+
|
| 254 |
+
### Connecting to an Existing Server
|
| 255 |
+
|
| 256 |
+
If you already have a Container Yard environment server running, you can connect directly:
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
from Container_Yard import ContainerYardEnv
|
| 260 |
+
|
| 261 |
+
# Connect to existing server
|
| 262 |
+
Container_Yardenv = ContainerYardEnv(base_url="<ENV_HTTP_URL_HERE>")
|
| 263 |
+
|
| 264 |
+
# Use as normal
|
| 265 |
+
result = Container_Yardenv.reset()
|
| 266 |
+
result = Container_Yardenv.step(ContainerYardAction(message="Hello!"))
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
Note: When connecting to an existing server, `Container_Yardenv.close()` will NOT stop the server.
|
| 270 |
+
|
| 271 |
+
### Using the Context Manager
|
| 272 |
+
|
| 273 |
+
The client supports context manager usage for automatic connection management:
|
| 274 |
+
|
| 275 |
+
```python
|
| 276 |
+
from Container_Yard import ContainerYardAction, ContainerYardEnv
|
| 277 |
+
|
| 278 |
+
# Connect with context manager (auto-connects and closes)
|
| 279 |
+
with ContainerYardEnv(base_url="http://localhost:8000") as env:
|
| 280 |
+
result = env.reset()
|
| 281 |
+
print(f"Reset: {result.observation.echoed_message}")
|
| 282 |
+
# Multiple steps with low latency
|
| 283 |
+
for msg in ["Hello", "World", "!"]:
|
| 284 |
+
result = env.step(ContainerYardAction(message=msg))
|
| 285 |
+
print(f"Echoed: {result.observation.echoed_message}")
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
The client uses WebSocket connections for:
|
| 289 |
+
- **Lower latency**: No HTTP connection overhead per request
|
| 290 |
+
- **Persistent session**: Server maintains your environment state
|
| 291 |
+
- **Efficient for episodes**: Better for many sequential steps
|
| 292 |
+
|
| 293 |
+
### Concurrent WebSocket Sessions
|
| 294 |
+
|
| 295 |
+
The server supports multiple concurrent WebSocket connections. To enable this,
|
| 296 |
+
modify `server/app.py` to use factory mode:
|
| 297 |
+
|
| 298 |
+
```python
|
| 299 |
+
# In server/app.py - use factory mode for concurrent sessions
|
| 300 |
+
app = create_app(
|
| 301 |
+
ContainerYardEnvironment, # Pass class, not instance
|
| 302 |
+
ContainerYardAction,
|
| 303 |
+
ContainerYardObservation,
|
| 304 |
+
max_concurrent_envs=4, # Allow 4 concurrent sessions
|
| 305 |
+
)
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
Then multiple clients can connect simultaneously:
|
| 309 |
+
|
| 310 |
+
```python
|
| 311 |
+
from Container_Yard import ContainerYardAction, ContainerYardEnv
|
| 312 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 313 |
+
|
| 314 |
+
def run_episode(client_id: int):
|
| 315 |
+
with ContainerYardEnv(base_url="http://localhost:8000") as env:
|
| 316 |
+
result = env.reset()
|
| 317 |
+
for i in range(10):
|
| 318 |
+
result = env.step(ContainerYardAction(message=f"Client {client_id}, step {i}"))
|
| 319 |
+
return client_id, result.observation.message_length
|
| 320 |
+
|
| 321 |
+
# Run 4 episodes concurrently
|
| 322 |
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 323 |
+
results = list(executor.map(run_episode, range(4)))
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
## Development & Testing
|
| 327 |
+
|
| 328 |
+
### Direct Environment Testing
|
| 329 |
+
|
| 330 |
+
Test the environment logic directly without starting the HTTP server:
|
| 331 |
+
|
| 332 |
+
```bash
|
| 333 |
+
# From the server directory
|
| 334 |
+
python3 server/Container_Yard_environment.py
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
This verifies that:
|
| 338 |
+
- Environment resets correctly
|
| 339 |
+
- Step executes actions properly
|
| 340 |
+
- State tracking works
|
| 341 |
+
- Rewards are calculated correctly
|
| 342 |
+
|
| 343 |
+
### Running Locally
|
| 344 |
+
|
| 345 |
+
Run the server locally for development:
|
| 346 |
+
|
| 347 |
+
```bash
|
| 348 |
+
uvicorn server.app:app --reload
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
## Project Structure
|
| 352 |
+
|
| 353 |
+
```
|
| 354 |
+
Container_Yard/
|
| 355 |
+
├── .dockerignore # Docker build exclusions
|
| 356 |
+
├── __init__.py # Module exports
|
| 357 |
+
├── README.md # This file
|
| 358 |
+
├── openenv.yaml # OpenEnv manifest
|
| 359 |
+
├── pyproject.toml # Project metadata and dependencies
|
| 360 |
+
├── uv.lock # Locked dependencies (generated)
|
| 361 |
+
├── client.py # ContainerYardEnv client
|
| 362 |
+
├── models.py # Action and Observation models
|
| 363 |
+
└── server/
|
| 364 |
+
├── __init__.py # Server module exports
|
| 365 |
+
├── Container_Yard_environment.py # Core environment logic
|
| 366 |
+
├── app.py # FastAPI application (HTTP + WebSocket endpoints)
|
| 367 |
+
└── Dockerfile # Container image definition
|
| 368 |
+
```
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Container Yard Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import ContainerYardEnv
|
| 10 |
+
from .models import ContainerYardAction, ContainerYardObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"ContainerYardAction",
|
| 14 |
+
"ContainerYardObservation",
|
| 15 |
+
"ContainerYardEnv",
|
| 16 |
+
]
|
__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (447 Bytes). View file
|
|
|
__pycache__/client.cpython-312.pyc
ADDED
|
Binary file (4.01 kB). View file
|
|
|
__pycache__/inference.cpython-312.pyc
ADDED
|
Binary file (8.74 kB). View file
|
|
|
__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (3.16 kB). View file
|
|
|
client.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Container Yard Environment Client."""
|
| 8 |
+
|
| 9 |
+
from typing import Dict
|
| 10 |
+
|
| 11 |
+
from openenv.core import EnvClient
|
| 12 |
+
from openenv.core.client_types import StepResult
|
| 13 |
+
from openenv.core.env_server.types import State
|
| 14 |
+
|
| 15 |
+
from .models import ContainerYardAction, ContainerYardObservation
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ContainerYardEnv(
|
| 19 |
+
EnvClient[ContainerYardAction, ContainerYardObservation, State]
|
| 20 |
+
):
|
| 21 |
+
"""
|
| 22 |
+
Client for the Container Yard Environment.
|
| 23 |
+
|
| 24 |
+
This client maintains a persistent WebSocket connection to the environment server,
|
| 25 |
+
enabling efficient multi-step interactions with lower latency.
|
| 26 |
+
Each client instance has its own dedicated environment session on the server.
|
| 27 |
+
|
| 28 |
+
Example:
|
| 29 |
+
>>> # Connect to a running server
|
| 30 |
+
>>> with ContainerYardEnv(base_url="http://localhost:8000") as client:
|
| 31 |
+
... result = client.reset()
|
| 32 |
+
... print(result.observation.echoed_message)
|
| 33 |
+
...
|
| 34 |
+
... result = client.step(ContainerYardAction(message="Hello!"))
|
| 35 |
+
... print(result.observation.echoed_message)
|
| 36 |
+
|
| 37 |
+
Example with Docker:
|
| 38 |
+
>>> # Automatically start container and connect
|
| 39 |
+
>>> client = ContainerYardEnv.from_docker_image("Container_Yard-env:latest")
|
| 40 |
+
>>> try:
|
| 41 |
+
... result = client.reset()
|
| 42 |
+
... result = client.step(ContainerYardAction(message="Test"))
|
| 43 |
+
... finally:
|
| 44 |
+
... client.close()
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def _step_payload(self, action: ContainerYardAction) -> Dict:
|
| 48 |
+
"""
|
| 49 |
+
Convert ContainerYardAction to JSON payload for step message.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
action: ContainerYardAction instance
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Dictionary representation suitable for JSON encoding
|
| 56 |
+
"""
|
| 57 |
+
return {
|
| 58 |
+
"stack_index": action.stack_index,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def _parse_result(self, payload: Dict) -> StepResult[ContainerYardObservation]:
|
| 62 |
+
"""
|
| 63 |
+
Parse server response into StepResult[ContainerYardObservation].
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
payload: JSON response data from server
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
StepResult with ContainerYardObservation
|
| 70 |
+
"""
|
| 71 |
+
obs_data = payload.get("observation", {})
|
| 72 |
+
observation = ContainerYardObservation(
|
| 73 |
+
stacks=obs_data.get("stacks", []),
|
| 74 |
+
containers_placed=obs_data.get("containers_placed", 0),
|
| 75 |
+
total_containers=obs_data.get("total_containers", 0),
|
| 76 |
+
current_container_id=obs_data.get("current_container_id", -1),
|
| 77 |
+
current_container_priority=obs_data.get("current_container_priority", 0),
|
| 78 |
+
rehandles_so_far=obs_data.get("rehandles_so_far", 0),
|
| 79 |
+
num_stacks=obs_data.get("num_stacks", 10),
|
| 80 |
+
max_stack_height=obs_data.get("max_stack_height", 5),
|
| 81 |
+
action_error=obs_data.get("action_error"),
|
| 82 |
+
done=payload.get("done", False),
|
| 83 |
+
reward=payload.get("reward", 0.0),
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
return StepResult(
|
| 87 |
+
observation=observation,
|
| 88 |
+
reward=payload.get("reward", 0.0),
|
| 89 |
+
done=payload.get("done", False),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 93 |
+
"""
|
| 94 |
+
Parse server response into State object.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
payload: JSON response from state request
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
State object with episode_id and step_count
|
| 101 |
+
"""
|
| 102 |
+
return State(
|
| 103 |
+
episode_id=payload.get("episode_id"),
|
| 104 |
+
step_count=payload.get("step_count", 0),
|
| 105 |
+
)
|
inference.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Inference script for Container Yard environment using OpenAI API.
|
| 9 |
+
|
| 10 |
+
This script evaluates a language model's ability to solve container yard placement
|
| 11 |
+
tasks using the hackathon-specified output format.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import json
|
| 17 |
+
from typing import Optional
|
| 18 |
+
|
| 19 |
+
# Load environment variables from .env file if it exists
|
| 20 |
+
try:
|
| 21 |
+
from dotenv import load_dotenv
|
| 22 |
+
load_dotenv()
|
| 23 |
+
except ImportError:
|
| 24 |
+
pass # python-dotenv not installed, use system env vars
|
| 25 |
+
|
| 26 |
+
from openai import OpenAI
|
| 27 |
+
|
| 28 |
+
# Read environment variables with defaults
|
| 29 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 30 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
|
| 31 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 32 |
+
|
| 33 |
+
if HF_TOKEN is None:
|
| 34 |
+
raise ValueError("HF_TOKEN environment variable is required")
|
| 35 |
+
|
| 36 |
+
# Initialize OpenAI client
|
| 37 |
+
client = OpenAI(
|
| 38 |
+
base_url=API_BASE_URL,
|
| 39 |
+
api_key=HF_TOKEN
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Import environment
|
| 43 |
+
from server.Container_Yard_environment import ContainerYardEnvironment
|
| 44 |
+
from models import ContainerYardAction
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def extract_stack_choice(response: str, num_stacks: int) -> Optional[int]:
|
| 48 |
+
"""
|
| 49 |
+
Extract stack choice from LLM response.
|
| 50 |
+
|
| 51 |
+
Looks for patterns like "stack 0", "stack=3", or just a number.
|
| 52 |
+
Returns None if extraction fails.
|
| 53 |
+
"""
|
| 54 |
+
response_lower = response.lower().strip()
|
| 55 |
+
|
| 56 |
+
# Try pattern: "stack X"
|
| 57 |
+
words = response_lower.split()
|
| 58 |
+
for i, word in enumerate(words):
|
| 59 |
+
if "stack" in word and i + 1 < len(words):
|
| 60 |
+
try:
|
| 61 |
+
stack_idx = int(words[i + 1])
|
| 62 |
+
if 0 <= stack_idx < num_stacks:
|
| 63 |
+
return stack_idx
|
| 64 |
+
except ValueError:
|
| 65 |
+
pass
|
| 66 |
+
|
| 67 |
+
# Try extracting any number
|
| 68 |
+
import re
|
| 69 |
+
numbers = re.findall(r'\d+', response_lower)
|
| 70 |
+
if numbers:
|
| 71 |
+
try:
|
| 72 |
+
stack_idx = int(numbers[0])
|
| 73 |
+
if 0 <= stack_idx < num_stacks:
|
| 74 |
+
return stack_idx
|
| 75 |
+
except (ValueError, IndexError):
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
return None
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def run_task(task_name: str = "medium") -> dict:
|
| 82 |
+
"""
|
| 83 |
+
Run a single task in the Container Yard environment.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
task_name: "easy", "medium", or "hard"
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
dict with episode results
|
| 90 |
+
"""
|
| 91 |
+
env = ContainerYardEnvironment(task_name=task_name)
|
| 92 |
+
obs = env.reset()
|
| 93 |
+
|
| 94 |
+
print(f"[START] task={task_name} env=container-yard model={MODEL_NAME}")
|
| 95 |
+
sys.stdout.flush()
|
| 96 |
+
|
| 97 |
+
step_count = 0
|
| 98 |
+
all_rewards = []
|
| 99 |
+
success = False
|
| 100 |
+
last_error = None
|
| 101 |
+
efficiency_score = 0.0
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
while not obs.done and step_count < 100:
|
| 105 |
+
step_count += 1
|
| 106 |
+
|
| 107 |
+
prompt = f"""You are managing a container yard.
|
| 108 |
+
|
| 109 |
+
Current state:
|
| 110 |
+
- Container to place: ID={obs.current_container_id}, Priority={obs.current_container_priority}
|
| 111 |
+
- Available stacks: {obs.num_stacks} stacks (0-{obs.num_stacks-1})
|
| 112 |
+
- Max stack height: {obs.max_stack_height}
|
| 113 |
+
- Current stacks: {json.dumps(obs.stacks)}
|
| 114 |
+
- Rehandles so far: {obs.rehandles_so_far}
|
| 115 |
+
|
| 116 |
+
Place the container in the stack that minimizes future rehandles.
|
| 117 |
+
Reply with ONLY the stack number (0-{obs.num_stacks-1}). No explanation needed."""
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
response = client.chat.completions.create(
|
| 121 |
+
model=MODEL_NAME,
|
| 122 |
+
messages=[{"role": "user", "content": prompt}],
|
| 123 |
+
temperature=0.7,
|
| 124 |
+
max_tokens=10,
|
| 125 |
+
)
|
| 126 |
+
action_str = (response.choices[0].message.content or "").strip()
|
| 127 |
+
except Exception as e:
|
| 128 |
+
action_str = "0"
|
| 129 |
+
last_error = str(e)
|
| 130 |
+
|
| 131 |
+
stack_idx = extract_stack_choice(action_str, obs.num_stacks)
|
| 132 |
+
if stack_idx is None:
|
| 133 |
+
stack_idx = 0
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
action = ContainerYardAction(stack_index=stack_idx)
|
| 137 |
+
obs = env.step(action)
|
| 138 |
+
|
| 139 |
+
reward_value = float(obs.reward or 0.0)
|
| 140 |
+
all_rewards.append(reward_value)
|
| 141 |
+
|
| 142 |
+
error_msg = obs.action_error if obs.action_error else "null"
|
| 143 |
+
print(
|
| 144 |
+
f"[STEP] step={step_count} action=place({stack_idx}) "
|
| 145 |
+
f"reward={reward_value:.2f} done={str(obs.done).lower()} error={error_msg}"
|
| 146 |
+
)
|
| 147 |
+
sys.stdout.flush()
|
| 148 |
+
|
| 149 |
+
if obs.done:
|
| 150 |
+
success = True
|
| 151 |
+
break
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
last_error = str(e)
|
| 155 |
+
print(f"[STEP] step={step_count} action=place({stack_idx}) reward=0.00 done=true error={last_error}")
|
| 156 |
+
sys.stdout.flush()
|
| 157 |
+
break
|
| 158 |
+
|
| 159 |
+
if step_count > 0:
|
| 160 |
+
efficiency_score = 1.0 - (obs.rehandles_so_far / max(obs.total_containers, 1))
|
| 161 |
+
success = success and step_count == obs.total_containers
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
last_error = str(e)
|
| 165 |
+
|
| 166 |
+
finally:
|
| 167 |
+
close_fn = getattr(env, "close", None)
|
| 168 |
+
if callable(close_fn):
|
| 169 |
+
try:
|
| 170 |
+
close_fn()
|
| 171 |
+
except Exception:
|
| 172 |
+
pass
|
| 173 |
+
|
| 174 |
+
rewards_str = ",".join([f"{r:.2f}" for r in all_rewards])
|
| 175 |
+
print(f"[END] success={str(success).lower()} steps={step_count} rewards={rewards_str}")
|
| 176 |
+
sys.stdout.flush()
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
"task": task_name,
|
| 180 |
+
"success": success,
|
| 181 |
+
"steps": step_count,
|
| 182 |
+
"total_rewards": sum(all_rewards),
|
| 183 |
+
"rehandles": obs.rehandles_so_far,
|
| 184 |
+
"efficiency": efficiency_score,
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def main():
|
| 189 |
+
"""Run all three tasks."""
|
| 190 |
+
tasks = ["easy", "medium", "hard"]
|
| 191 |
+
results = []
|
| 192 |
+
|
| 193 |
+
for task in tasks:
|
| 194 |
+
try:
|
| 195 |
+
result = run_task(task)
|
| 196 |
+
results.append(result)
|
| 197 |
+
except Exception as e:
|
| 198 |
+
print(f"[ERROR] Task {task} failed: {e}", file=sys.stderr)
|
| 199 |
+
results.append({
|
| 200 |
+
"task": task,
|
| 201 |
+
"success": False,
|
| 202 |
+
"steps": 0,
|
| 203 |
+
"total_rewards": 0.0,
|
| 204 |
+
"error": str(e),
|
| 205 |
+
})
|
| 206 |
+
|
| 207 |
+
# Summary
|
| 208 |
+
print("\n=== Summary ===", file=sys.stderr)
|
| 209 |
+
for result in results:
|
| 210 |
+
print(f"Task {result['task']}: success={result['success']}, efficiency={result.get('efficiency', 0.0):.2f}", file=sys.stderr)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Data models for the Container Yard Environment.
|
| 9 |
+
|
| 10 |
+
The Container Yard environment simulates a port container yard where containers
|
| 11 |
+
arrive sequentially and must be placed into stacks to minimize rehandles during
|
| 12 |
+
retrieval operations.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from typing import List, Optional
|
| 16 |
+
from openenv.core.env_server.types import Action, Observation
|
| 17 |
+
from pydantic import Field
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Container:
|
| 21 |
+
"""Represents a single container with ID and priority."""
|
| 22 |
+
|
| 23 |
+
def __init__(self, container_id: int, priority: int):
|
| 24 |
+
self.container_id = container_id
|
| 25 |
+
priority_map = {1: 0, 2: 1, 3: 2} # 1=earliest (0), 3=latest (2)
|
| 26 |
+
self.retrieval_priority = priority_map.get(priority, 0)
|
| 27 |
+
|
| 28 |
+
def __repr__(self):
|
| 29 |
+
return f"C{self.container_id}(P{self.retrieval_priority})"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ContainerYardAction(Action):
|
| 33 |
+
"""Action to place a container in a specific stack."""
|
| 34 |
+
|
| 35 |
+
stack_index: int = Field(..., description="Index of the stack to place container in (0-9)")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ContainerYardObservation(Observation):
|
| 39 |
+
"""Observation of the current container yard state."""
|
| 40 |
+
|
| 41 |
+
stacks: List[List[int]] = Field(
|
| 42 |
+
default_factory=list,
|
| 43 |
+
description="Current state of stacks (container IDs, -1 means empty slot)"
|
| 44 |
+
)
|
| 45 |
+
containers_placed: int = Field(default=0, description="Number of containers placed so far")
|
| 46 |
+
total_containers: int = Field(default=0, description="Total containers in this episode")
|
| 47 |
+
current_container_id: int = Field(default=-1, description="ID of current container to place (-1 if done)")
|
| 48 |
+
current_container_priority: int = Field(default=0, description="Priority of current container (1-3)")
|
| 49 |
+
rehandles_so_far: int = Field(default=0, description="Total rehandles occurred so far")
|
| 50 |
+
num_stacks: int = Field(default=10, description="Number of stacks in the yard")
|
| 51 |
+
max_stack_height: int = Field(default=5, description="Maximum height of each stack")
|
| 52 |
+
action_error: Optional[str] = Field(default=None, description="Error message if last action failed")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: Container_Yard
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
description: |
|
| 8 |
+
Container Yard Port Operations Simulation
|
| 9 |
+
Simulates a real-world port container yard where agents must place arriving containers
|
| 10 |
+
into stacks to minimize rehandles during retrieval operations.
|
| 11 |
+
metadata:
|
| 12 |
+
difficulty_levels:
|
| 13 |
+
- easy
|
| 14 |
+
- medium
|
| 15 |
+
- hard
|
| 16 |
+
tasks: 3
|
| 17 |
+
observation_space: Stack states with container priorities
|
| 18 |
+
action_space: Discrete(10) - stack selection
|
| 19 |
+
reward: Incremental feedback based on placement efficiency
|
| 20 |
+
max_container_height: 3-5 (task dependent)
|
pyproject.toml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-Container_Yard"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Container Yard environment for OpenEnv - port container yard simulation"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# Core OpenEnv runtime
|
| 18 |
+
"openenv-core[core]>=0.2.2",
|
| 19 |
+
# LLM inference
|
| 20 |
+
"openai>=1.0.0",
|
| 21 |
+
# Additional dependencies
|
| 22 |
+
"pydantic>=2.0.0",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
[project.optional-dependencies]
|
| 26 |
+
dev = [
|
| 27 |
+
"pytest>=8.0.0",
|
| 28 |
+
"pytest-cov>=4.0.0",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
[project.scripts]
|
| 32 |
+
# Server entry point - enables running via: uv run --project . server
|
| 33 |
+
# or: python -m Container_Yard.server.app
|
| 34 |
+
server = "Container_Yard.server.app:main"
|
| 35 |
+
|
| 36 |
+
[tool.setuptools]
|
| 37 |
+
include-package-data = true
|
| 38 |
+
packages = ["Container_Yard", "Container_Yard.server"]
|
| 39 |
+
package-dir = { "Container_Yard" = ".", "Container_Yard.server" = "server" }
|
quickstart.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Quick start guide for Container Yard Environment
|
| 3 |
+
|
| 4 |
+
echo "Container Yard Environment - Quick Start"
|
| 5 |
+
echo "=========================================="
|
| 6 |
+
echo ""
|
| 7 |
+
|
| 8 |
+
# Check if HF_TOKEN is set
|
| 9 |
+
if [ -z "$HF_TOKEN" ]; then
|
| 10 |
+
echo "⚠️ HF_TOKEN not set! This is required for inference.py"
|
| 11 |
+
echo ""
|
| 12 |
+
echo "Set it with:"
|
| 13 |
+
echo " export HF_TOKEN='your-hugging-face-token'"
|
| 14 |
+
echo ""
|
| 15 |
+
else
|
| 16 |
+
echo "✓ HF_TOKEN is set"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# Show environment variable options
|
| 20 |
+
echo ""
|
| 21 |
+
echo "Optional environment variables:"
|
| 22 |
+
echo " API_BASE_URL (default: https://api.openai.com/v1)"
|
| 23 |
+
echo " MODEL_NAME (default: gpt-4o-mini)"
|
| 24 |
+
echo ""
|
| 25 |
+
|
| 26 |
+
# Test local environment
|
| 27 |
+
echo "Testing local environment..."
|
| 28 |
+
python -c "
|
| 29 |
+
import sys
|
| 30 |
+
sys.path.insert(0, '.')
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
from server.Container_Yard_environment import ContainerYardEnvironment
|
| 34 |
+
from models import ContainerYardAction
|
| 35 |
+
print('✓ Environment imports OK')
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f'✗ Import error: {e}')
|
| 38 |
+
sys.exit(1)
|
| 39 |
+
|
| 40 |
+
# Quick environment test
|
| 41 |
+
env = ContainerYardEnvironment('easy')
|
| 42 |
+
obs = env.reset()
|
| 43 |
+
print(f'✓ Environment reset OK ({obs.total_containers} containers)')
|
| 44 |
+
" && echo "" && echo "✓ Ready to run inference!" || echo "✗ Setup failed"
|
| 45 |
+
|
| 46 |
+
echo ""
|
| 47 |
+
echo "To run inference:"
|
| 48 |
+
echo " python inference.py"
|
| 49 |
+
echo ""
|
| 50 |
+
echo "To test Docker build:"
|
| 51 |
+
echo " docker build -t container-yard ."
|
| 52 |
+
echo " docker run -p 8000:8000 container-yard"
|
server/Container_Yard_environment.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Container Yard Environment Implementation.
|
| 9 |
+
|
| 10 |
+
Simulates a port container yard where containers arrive sequentially with different
|
| 11 |
+
retrieval priorities (1-3). The objective is to place containers into stacks to
|
| 12 |
+
minimize rehandles during retrieval operations.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from uuid import uuid4
|
| 16 |
+
from typing import List, Tuple
|
| 17 |
+
import random
|
| 18 |
+
|
| 19 |
+
from openenv.core.env_server.interfaces import Environment
|
| 20 |
+
from openenv.core.env_server.types import State
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from ..models import ContainerYardAction, ContainerYardObservation, Container
|
| 24 |
+
except ImportError:
|
| 25 |
+
from models import ContainerYardAction, ContainerYardObservation, Container
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ContainerYardEnvironment(Environment):
|
| 29 |
+
"""
|
| 30 |
+
Container Yard environment for the hackathon challenge.
|
| 31 |
+
|
| 32 |
+
Containers arrive with priorities 1-3 (1=earliest retrieval, 3=latest).
|
| 33 |
+
Each stack can hold up to max_stack_height containers.
|
| 34 |
+
Agents must place containers to minimize rehandles during retrieval.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 38 |
+
|
| 39 |
+
def __init__(self, task_name: str = "medium"):
|
| 40 |
+
"""
|
| 41 |
+
Initialize the Container Yard environment.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
task_name: "easy" (5 containers, all priority 1),
|
| 45 |
+
"medium" (10 containers, priority 1-2),
|
| 46 |
+
"hard" (15 containers, priority 1-3)
|
| 47 |
+
"""
|
| 48 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 49 |
+
self.task_name = task_name
|
| 50 |
+
self._setup_task(task_name)
|
| 51 |
+
|
| 52 |
+
# Environment state
|
| 53 |
+
self.containers: List[Container] = []
|
| 54 |
+
self.stacks: List[List[int]] = [[] for _ in range(self.num_stacks)]
|
| 55 |
+
self.current_container_idx = 0
|
| 56 |
+
self.rehandles = 0
|
| 57 |
+
self.placement_history: List[Tuple[int, int]] = [] # (container_id, stack_idx)
|
| 58 |
+
|
| 59 |
+
def _setup_task(self, task_name: str):
|
| 60 |
+
"""Configure environment parameters based on task difficulty."""
|
| 61 |
+
tasks = {
|
| 62 |
+
"easy": {"num_containers": 5, "num_stacks": 5, "max_height": 5, "priorities": [1, 1, 1, 1, 1]},
|
| 63 |
+
"medium": {"num_containers": 10, "num_stacks": 8, "max_height": 4,
|
| 64 |
+
"priorities": [1, 1, 1, 1, 2, 2, 2, 1, 2, 1]},
|
| 65 |
+
"hard": {"num_containers": 15, "num_stacks": 10, "max_height": 3,
|
| 66 |
+
"priorities": [1, 2, 1, 3, 2, 1, 3, 2, 1, 2, 3, 1, 2, 3, 1]},
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
config = tasks.get(task_name, tasks["medium"])
|
| 70 |
+
self.num_containers = config["num_containers"]
|
| 71 |
+
self.num_stacks = config["num_stacks"]
|
| 72 |
+
self.max_stack_height = config["max_height"]
|
| 73 |
+
self.priorities = config["priorities"]
|
| 74 |
+
|
| 75 |
+
def reset(self) -> ContainerYardObservation:
|
| 76 |
+
"""
|
| 77 |
+
Reset the environment for a new episode.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
Initial observation
|
| 81 |
+
"""
|
| 82 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 83 |
+
|
| 84 |
+
# Initialize containers with priorities
|
| 85 |
+
self.containers = [
|
| 86 |
+
Container(i, self.priorities[i]) for i in range(self.num_containers)
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
# Shuffle container arrival order
|
| 90 |
+
random.shuffle(self.containers)
|
| 91 |
+
|
| 92 |
+
self.stacks = [[] for _ in range(self.num_stacks)]
|
| 93 |
+
self.current_container_idx = 0
|
| 94 |
+
self.rehandles = 0
|
| 95 |
+
self.placement_history = []
|
| 96 |
+
|
| 97 |
+
return self._get_observation(action_error=None)
|
| 98 |
+
|
| 99 |
+
def step(self, action: ContainerYardAction) -> ContainerYardObservation:
|
| 100 |
+
"""
|
| 101 |
+
Execute one step: place current container in specified stack.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
action: ContainerYardAction with stack_index
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
ContainerYardObservation with updated yard state
|
| 108 |
+
"""
|
| 109 |
+
self._state.step_count += 1
|
| 110 |
+
error = None
|
| 111 |
+
|
| 112 |
+
stack_idx = action.stack_index
|
| 113 |
+
|
| 114 |
+
# Validate action
|
| 115 |
+
if stack_idx < 0 or stack_idx >= self.num_stacks:
|
| 116 |
+
error = f"Invalid stack index {stack_idx}. Valid range: 0-{self.num_stacks-1}"
|
| 117 |
+
return self._get_observation(action_error=error)
|
| 118 |
+
|
| 119 |
+
if len(self.stacks[stack_idx]) >= self.max_stack_height:
|
| 120 |
+
error = f"Stack {stack_idx} is full (height={len(self.stacks[stack_idx])})"
|
| 121 |
+
return self._get_observation(action_error=error)
|
| 122 |
+
|
| 123 |
+
# Place container
|
| 124 |
+
container = self.containers[self.current_container_idx]
|
| 125 |
+
self.stacks[stack_idx].append(container.container_id)
|
| 126 |
+
self.placement_history.append((container.container_id, stack_idx))
|
| 127 |
+
|
| 128 |
+
# Check for rehandles caused by this placement
|
| 129 |
+
rehandles_caused = self._count_rehandles_from_placement(stack_idx, container)
|
| 130 |
+
self.rehandles += rehandles_caused
|
| 131 |
+
|
| 132 |
+
self.current_container_idx += 1
|
| 133 |
+
done = (self.current_container_idx >= self.num_containers)
|
| 134 |
+
|
| 135 |
+
# Compute reward
|
| 136 |
+
reward = self._compute_reward(container, stack_idx, rehandles_caused)
|
| 137 |
+
|
| 138 |
+
obs = self._get_observation(action_error=error)
|
| 139 |
+
obs.reward = reward
|
| 140 |
+
obs.done = done
|
| 141 |
+
|
| 142 |
+
return obs
|
| 143 |
+
|
| 144 |
+
def _count_rehandles_from_placement(self, stack_idx: int, container: Container) -> int:
|
| 145 |
+
"""
|
| 146 |
+
Count how many containers in this stack would need to be rehandled
|
| 147 |
+
because this container is placed on top of them.
|
| 148 |
+
|
| 149 |
+
Rehandle: container X is in stack with container Y below it,
|
| 150 |
+
but X has LOWER priority (earlier retrieval) than Y.
|
| 151 |
+
"""
|
| 152 |
+
rehandles = 0
|
| 153 |
+
stack = self.stacks[stack_idx]
|
| 154 |
+
|
| 155 |
+
if len(stack) < 2:
|
| 156 |
+
return rehandles
|
| 157 |
+
|
| 158 |
+
# Check all containers below the newly placed one
|
| 159 |
+
newly_placed_priority = container.retrieval_priority
|
| 160 |
+
for i in range(len(stack) - 1):
|
| 161 |
+
below_id = stack[i]
|
| 162 |
+
# Find the container with this ID to get its priority
|
| 163 |
+
below_container = next(c for c in self.containers if c.container_id == below_id)
|
| 164 |
+
if below_container.retrieval_priority > newly_placed_priority:
|
| 165 |
+
rehandles += 1
|
| 166 |
+
|
| 167 |
+
return rehandles
|
| 168 |
+
|
| 169 |
+
def _compute_reward(self, container: Container, stack_idx: int, rehandles_caused: int) -> float:
|
| 170 |
+
"""Compute reward for placing a container."""
|
| 171 |
+
reward = 0.0
|
| 172 |
+
|
| 173 |
+
# Base reward: successful placement
|
| 174 |
+
reward += 0.1
|
| 175 |
+
|
| 176 |
+
# Penalty for rehandles
|
| 177 |
+
reward -= rehandles_caused * 0.5
|
| 178 |
+
|
| 179 |
+
# Bonus for efficient placement
|
| 180 |
+
if rehandles_caused == 0:
|
| 181 |
+
reward += 0.3
|
| 182 |
+
|
| 183 |
+
# Bonus for stacking containers with same priority
|
| 184 |
+
stack = self.stacks[stack_idx]
|
| 185 |
+
if len(stack) > 1:
|
| 186 |
+
below_id = stack[-2]
|
| 187 |
+
below_container = next(c for c in self.containers if c.container_id == below_id)
|
| 188 |
+
if below_container.retrieval_priority == container.retrieval_priority:
|
| 189 |
+
reward += 0.2
|
| 190 |
+
|
| 191 |
+
return reward
|
| 192 |
+
|
| 193 |
+
def _get_observation(self, action_error: str = None) -> ContainerYardObservation:
|
| 194 |
+
"""Build observation from current state."""
|
| 195 |
+
current_id = -1
|
| 196 |
+
current_priority = 0
|
| 197 |
+
|
| 198 |
+
if self.current_container_idx < len(self.containers):
|
| 199 |
+
current_id = self.containers[self.current_container_idx].container_id
|
| 200 |
+
current_priority = self.containers[self.current_container_idx].retrieval_priority + 1
|
| 201 |
+
|
| 202 |
+
# Stacks already contain container IDs directly
|
| 203 |
+
stacks_data = [list(stack) for stack in self.stacks]
|
| 204 |
+
|
| 205 |
+
return ContainerYardObservation(
|
| 206 |
+
stacks=stacks_data,
|
| 207 |
+
containers_placed=self.current_container_idx,
|
| 208 |
+
total_containers=self.num_containers,
|
| 209 |
+
current_container_id=current_id,
|
| 210 |
+
current_container_priority=current_priority,
|
| 211 |
+
rehandles_so_far=self.rehandles,
|
| 212 |
+
num_stacks=self.num_stacks,
|
| 213 |
+
max_stack_height=self.max_stack_height,
|
| 214 |
+
action_error=action_error,
|
| 215 |
+
done=(self.current_container_idx >= self.num_containers),
|
| 216 |
+
reward=0.0,
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
@property
|
| 220 |
+
def state(self) -> State:
|
| 221 |
+
"""Get current environment state."""
|
| 222 |
+
return self._state
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Container Yard environment server components."""
|
| 8 |
+
|
| 9 |
+
from .Container_Yard_environment import ContainerYardEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["ContainerYardEnvironment"]
|
server/__pycache__/Container_Yard_environment.cpython-312.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
server/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (326 Bytes). View file
|
|
|
server/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (5.55 kB). View file
|
|
|
server/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
FastAPI application for the Container Yard Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the ContainerYardEnvironment
|
| 11 |
+
over HTTP and WebSocket endpoints, compatible with EnvClient.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
- POST /reset: Reset the environment
|
| 15 |
+
- POST /step: Execute an action
|
| 16 |
+
- GET /state: Get current environment state
|
| 17 |
+
- GET /schema: Get action/observation schemas
|
| 18 |
+
- WS /ws: WebSocket endpoint for persistent sessions
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# Development (with auto-reload):
|
| 22 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 23 |
+
|
| 24 |
+
# Production:
|
| 25 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 26 |
+
|
| 27 |
+
# Or run directly:
|
| 28 |
+
python -m server.app
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
except Exception as e: # pragma: no cover
|
| 34 |
+
raise ImportError(
|
| 35 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 36 |
+
) from e
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
from ..models import ContainerYardAction, ContainerYardObservation
|
| 40 |
+
from .Container_Yard_environment import ContainerYardEnvironment
|
| 41 |
+
except ModuleNotFoundError:
|
| 42 |
+
from models import ContainerYardAction, ContainerYardObservation
|
| 43 |
+
from server.Container_Yard_environment import ContainerYardEnvironment
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# Create the app with web interface and README integration
|
| 47 |
+
app = create_app(
|
| 48 |
+
ContainerYardEnvironment,
|
| 49 |
+
ContainerYardAction,
|
| 50 |
+
ContainerYardObservation,
|
| 51 |
+
env_name="Container_Yard",
|
| 52 |
+
max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 57 |
+
"""
|
| 58 |
+
Entry point for direct execution via uv run or python -m.
|
| 59 |
+
|
| 60 |
+
This function enables running the server without Docker:
|
| 61 |
+
uv run --project . server
|
| 62 |
+
uv run --project . server --port 8001
|
| 63 |
+
python -m Container_Yard.server.app
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
host: Host address to bind to (default: "0.0.0.0")
|
| 67 |
+
port: Port number to listen on (default: 8000)
|
| 68 |
+
|
| 69 |
+
For production deployments, consider using uvicorn directly with
|
| 70 |
+
multiple workers:
|
| 71 |
+
uvicorn Container_Yard.server.app:app --workers 4
|
| 72 |
+
"""
|
| 73 |
+
import uvicorn
|
| 74 |
+
|
| 75 |
+
uvicorn.run(app, host=host, port=port)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
import argparse
|
| 80 |
+
|
| 81 |
+
parser = argparse.ArgumentParser()
|
| 82 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 83 |
+
args = parser.parse_args()
|
| 84 |
+
main(port=args.port)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
test_env.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple test script to validate the Container Yard environment locally.
|
| 4 |
+
This doesn't require OpenAI API or a running server.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
sys.path.insert(0, '.')
|
| 9 |
+
|
| 10 |
+
from server.Container_Yard_environment import ContainerYardEnvironment
|
| 11 |
+
from models import ContainerYardAction
|
| 12 |
+
|
| 13 |
+
def test_task(task_name: str, num_steps: int = 100):
|
| 14 |
+
"""Test a single task."""
|
| 15 |
+
print(f"\n{'='*60}")
|
| 16 |
+
print(f"Testing {task_name.upper()} task")
|
| 17 |
+
print(f"{'='*60}")
|
| 18 |
+
|
| 19 |
+
env = ContainerYardEnvironment(task_name=task_name)
|
| 20 |
+
obs = env.reset()
|
| 21 |
+
|
| 22 |
+
print(f"Initial state:")
|
| 23 |
+
print(f" - Containers: {obs.total_containers}")
|
| 24 |
+
print(f" - Stacks: {obs.num_stacks}")
|
| 25 |
+
print(f" - Max height: {obs.max_stack_height}")
|
| 26 |
+
|
| 27 |
+
step_count = 0
|
| 28 |
+
total_reward = 0.0
|
| 29 |
+
|
| 30 |
+
while not obs.done and step_count < num_steps:
|
| 31 |
+
step_count += 1
|
| 32 |
+
|
| 33 |
+
# Simple greedy strategy: place in stack with fewest containers
|
| 34 |
+
stack_sizes = [len(stack) for stack in obs.stacks]
|
| 35 |
+
best_stack = min(range(len(stack_sizes)), key=lambda i: stack_sizes[i])
|
| 36 |
+
|
| 37 |
+
action = ContainerYardAction(stack_index=best_stack)
|
| 38 |
+
obs = env.step(action)
|
| 39 |
+
total_reward += obs.reward
|
| 40 |
+
|
| 41 |
+
if step_count <= 3 or obs.done:
|
| 42 |
+
print(f"Step {step_count}: Placed C{obs.current_container_id-1 if obs.current_container_id > 0 else 'X'} "
|
| 43 |
+
f"in stack {best_stack}, reward={obs.reward:.2f}, "
|
| 44 |
+
f"rehandles={obs.rehandles_so_far}")
|
| 45 |
+
|
| 46 |
+
efficiency = 1.0 - (obs.rehandles_so_far / obs.total_containers)
|
| 47 |
+
|
| 48 |
+
print(f"\nFinal results:")
|
| 49 |
+
print(f" - Steps taken: {step_count}")
|
| 50 |
+
print(f" - Total reward: {total_reward:.2f}")
|
| 51 |
+
print(f" - Total rehandles: {obs.rehandles_so_far}")
|
| 52 |
+
print(f" - Efficiency score: {efficiency:.2f}")
|
| 53 |
+
print(f" - Success: {obs.rehandles_so_far <= 3 * (len(obs.stacks) // 5)}")
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
'task': task_name,
|
| 57 |
+
'steps': step_count,
|
| 58 |
+
'rehandles': obs.rehandles_so_far,
|
| 59 |
+
'efficiency': efficiency,
|
| 60 |
+
'total_reward': total_reward,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
print("Container Yard Environment - Local Test")
|
| 65 |
+
|
| 66 |
+
results = []
|
| 67 |
+
for task in ['easy', 'medium', 'hard']:
|
| 68 |
+
try:
|
| 69 |
+
result = test_task(task)
|
| 70 |
+
results.append(result)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"ERROR in {task} task: {e}")
|
| 73 |
+
import traceback
|
| 74 |
+
traceback.print_exc()
|
| 75 |
+
|
| 76 |
+
print(f"\n{'='*60}")
|
| 77 |
+
print("Summary")
|
| 78 |
+
print(f"{'='*60}")
|
| 79 |
+
for r in results:
|
| 80 |
+
print(f"{r['task']:8} | Steps: {r['steps']:2} | Rehandles: {r['rehandles']:2} | Efficiency: {r['efficiency']:.2f}")
|