Draken1606 commited on
Commit
cc75d6e
·
0 Parent(s):

Initial Container Yard env submission

Browse files
.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}")