Update CERNenv Space
Browse files- server/app.py +105 -3
- space/training/app.py +22 -22
server/app.py
CHANGED
|
@@ -1,16 +1,107 @@
|
|
| 1 |
-
"""FastAPI app exposing ``CERNCollisionEnvironment`` over the OpenEnv HTTP API.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import os
|
| 6 |
from typing import Optional
|
| 7 |
|
|
|
|
| 8 |
from openenv.core.env_server import create_fastapi_app
|
| 9 |
|
| 10 |
from models import CollisionObservation, ExperimentAction
|
| 11 |
from server.environment import CERNCollisionEnvironment
|
| 12 |
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
def make_env_factory(
|
| 15 |
max_steps: int,
|
| 16 |
default_difficulty: Optional[str],
|
|
@@ -29,9 +120,20 @@ def build_app(
|
|
| 29 |
max_steps: int = 40,
|
| 30 |
default_difficulty: Optional[str] = None,
|
| 31 |
):
|
| 32 |
-
"""Construct the FastAPI app with a per-session environment factory.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
factory = make_env_factory(max_steps=max_steps, default_difficulty=default_difficulty)
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
app = build_app(
|
|
|
|
| 1 |
+
"""FastAPI app exposing ``CERNCollisionEnvironment`` over the OpenEnv HTTP API.
|
| 2 |
+
|
| 3 |
+
We delegate the standard OpenEnv routes (``/reset``, ``/step``, ``/state``,
|
| 4 |
+
``/schema``, ``/health``, ``/mcp``) to ``create_fastapi_app`` and add a
|
| 5 |
+
human-friendly landing page at ``/`` so the Hugging Face Space preview
|
| 6 |
+
shows the project description instead of a 404.
|
| 7 |
+
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
import os
|
| 12 |
from typing import Optional
|
| 13 |
|
| 14 |
+
from fastapi.responses import HTMLResponse
|
| 15 |
from openenv.core.env_server import create_fastapi_app
|
| 16 |
|
| 17 |
from models import CollisionObservation, ExperimentAction
|
| 18 |
from server.environment import CERNCollisionEnvironment
|
| 19 |
|
| 20 |
|
| 21 |
+
_LANDING_PAGE = """\
|
| 22 |
+
<!doctype html>
|
| 23 |
+
<html lang=en>
|
| 24 |
+
<head>
|
| 25 |
+
<meta charset=utf-8>
|
| 26 |
+
<title>CERNenv — LHC Discovery RL Environment</title>
|
| 27 |
+
<meta name=viewport content="width=device-width,initial-scale=1">
|
| 28 |
+
<style>
|
| 29 |
+
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
| 30 |
+
margin: 2rem auto; max-width: 780px; color:#111; padding: 0 1rem; line-height:1.5 }
|
| 31 |
+
h1 { margin-bottom: .25rem }
|
| 32 |
+
h2 { margin-top: 2rem; border-bottom: 1px solid #eee; padding-bottom: .25rem }
|
| 33 |
+
code { background:#f4f4f4; padding:.05rem .35rem; border-radius:4px; font-size:.95em }
|
| 34 |
+
pre { background:#0e1116; color:#e6edf3; padding:1rem; border-radius:6px; overflow-x:auto }
|
| 35 |
+
pre code { background:transparent; color:inherit; padding:0 }
|
| 36 |
+
.pill { display:inline-block; padding:.1rem .55rem; border-radius:999px;
|
| 37 |
+
background:#e8f0ff; color:#1d4ed8; font-size:.85em; margin-right:.25rem }
|
| 38 |
+
.muted { color:#666 }
|
| 39 |
+
a { color:#1d4ed8 }
|
| 40 |
+
table { border-collapse:collapse; margin: .5rem 0 }
|
| 41 |
+
th,td { text-align:left; padding:.25rem .9rem .25rem 0; vertical-align: top }
|
| 42 |
+
th { color:#444; font-weight:600 }
|
| 43 |
+
</style>
|
| 44 |
+
</head>
|
| 45 |
+
<body>
|
| 46 |
+
<h1>⚛️ CERNenv</h1>
|
| 47 |
+
<p class=muted>An LHC (Large Hadron Collider) particle-discovery RL environment for autonomous physicist agents — built for the Meta OpenEnv Hackathon.</p>
|
| 48 |
+
|
| 49 |
+
<p>
|
| 50 |
+
<span class=pill>OpenEnv</span>
|
| 51 |
+
<span class=pill>POMDP</span>
|
| 52 |
+
<span class=pill>16 action types</span>
|
| 53 |
+
<span class=pill>3 difficulty levels</span>
|
| 54 |
+
<span class=pill>HTTP + WebSocket</span>
|
| 55 |
+
</p>
|
| 56 |
+
|
| 57 |
+
<h2>What this is</h2>
|
| 58 |
+
<p>
|
| 59 |
+
A Large Language Model (LLM) agent plays a high-energy physicist running an
|
| 60 |
+
analysis at the LHC. Each step it picks one structured action — configure
|
| 61 |
+
the beam, allocate luminosity, set a trigger, collect collisions, fit a
|
| 62 |
+
resonance, estimate significance, submit a discovery claim, and so on —
|
| 63 |
+
and receives a noisy detector-style observation. The latent particle
|
| 64 |
+
(mass, decay channel, branching ratios, width) is hidden ground truth.
|
| 65 |
+
Reward decomposes into per-step shaping + a dominant terminal calibration
|
| 66 |
+
against the truth particle.
|
| 67 |
+
</p>
|
| 68 |
+
|
| 69 |
+
<h2>API</h2>
|
| 70 |
+
<table>
|
| 71 |
+
<tr><th><code>GET /health</code></th><td>liveness probe</td></tr>
|
| 72 |
+
<tr><th><code>GET /schema</code></th><td>JSON schemas for actions, observations, state</td></tr>
|
| 73 |
+
<tr><th><code>POST /reset</code></th><td>start a new episode (e.g. <code>{"seed": 7, "scenario": "easy_diphoton_160"}</code>)</td></tr>
|
| 74 |
+
<tr><th><code>POST /step</code></th><td>execute one action (<code>{"action": {"action_type": ..., "parameters": {...}, "justification": "..."}}</code>)</td></tr>
|
| 75 |
+
<tr><th><code>GET /state</code></th><td>current public state snapshot</td></tr>
|
| 76 |
+
<tr><th><code>GET /docs</code></th><td>interactive Swagger UI</td></tr>
|
| 77 |
+
<tr><th><code>GET /metadata</code></th><td>environment metadata</td></tr>
|
| 78 |
+
</table>
|
| 79 |
+
|
| 80 |
+
<h2>Quickstart</h2>
|
| 81 |
+
<pre><code># reset
|
| 82 |
+
curl -X POST $URL/reset \\
|
| 83 |
+
-H 'Content-Type: application/json' \\
|
| 84 |
+
-d '{"seed": 7, "scenario": "easy_diphoton_160"}'
|
| 85 |
+
|
| 86 |
+
# step
|
| 87 |
+
curl -X POST $URL/step \\
|
| 88 |
+
-H 'Content-Type: application/json' \\
|
| 89 |
+
-d '{"action": {"action_type": "configure_beam",
|
| 90 |
+
"parameters": {"sqrt_s_tev": 13.0},
|
| 91 |
+
"justification": "set 13 TeV"}}'</code></pre>
|
| 92 |
+
|
| 93 |
+
<h2>Companion Spaces</h2>
|
| 94 |
+
<ul>
|
| 95 |
+
<li>📓 Trainer (Unsloth + LoRA + GRPO on A100): <a href="https://huggingface.co/spaces/anugrah55/cernenv-trainer">anugrah55/cernenv-trainer</a></li>
|
| 96 |
+
<li>🎯 Trained adapters (LoRA): <a href="https://huggingface.co/anugrah55/cernenv-grpo-qwen2.5-3b">anugrah55/cernenv-grpo-qwen2.5-3b</a></li>
|
| 97 |
+
</ul>
|
| 98 |
+
|
| 99 |
+
<p class=muted style="margin-top:2rem">CERNenv · OpenEnv-compatible · BSD-3-Clause</p>
|
| 100 |
+
</body>
|
| 101 |
+
</html>
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
|
| 105 |
def make_env_factory(
|
| 106 |
max_steps: int,
|
| 107 |
default_difficulty: Optional[str],
|
|
|
|
| 120 |
max_steps: int = 40,
|
| 121 |
default_difficulty: Optional[str] = None,
|
| 122 |
):
|
| 123 |
+
"""Construct the FastAPI app with a per-session environment factory.
|
| 124 |
+
|
| 125 |
+
The OpenEnv-provided routes (`/reset`, `/step`, `/state`, `/schema`,
|
| 126 |
+
`/health`, `/mcp`) come from ``create_fastapi_app``. We then mount a
|
| 127 |
+
friendly landing page at ``/`` so the Space preview is informative.
|
| 128 |
+
"""
|
| 129 |
factory = make_env_factory(max_steps=max_steps, default_difficulty=default_difficulty)
|
| 130 |
+
fa_app = create_fastapi_app(factory, ExperimentAction, CollisionObservation)
|
| 131 |
+
|
| 132 |
+
@fa_app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
| 133 |
+
def landing() -> HTMLResponse: # pragma: no cover - trivial
|
| 134 |
+
return HTMLResponse(_LANDING_PAGE)
|
| 135 |
+
|
| 136 |
+
return fa_app
|
| 137 |
|
| 138 |
|
| 139 |
app = build_app(
|
space/training/app.py
CHANGED
|
@@ -297,17 +297,17 @@ _HTML = """\
|
|
| 297 |
<meta charset=utf-8>
|
| 298 |
<title>CERNenv Trainer</title>
|
| 299 |
<style>
|
| 300 |
-
body {
|
| 301 |
-
h1 {
|
| 302 |
-
.muted {
|
| 303 |
-
pre {
|
| 304 |
-
button {
|
| 305 |
-
.pill {
|
| 306 |
-
.ok {
|
| 307 |
-
.fail {
|
| 308 |
-
.run {
|
| 309 |
-
table {
|
| 310 |
-
td {
|
| 311 |
</style>
|
| 312 |
</head>
|
| 313 |
<body>
|
|
@@ -326,31 +326,31 @@ _HTML = """\
|
|
| 326 |
<pre id=logs>loading…</pre>
|
| 327 |
|
| 328 |
<script>
|
| 329 |
-
async function refresh() {
|
| 330 |
const s = await fetch('/status').then(r => r.json());
|
| 331 |
const pill = document.getElementById('status');
|
| 332 |
pill.textContent = s.status;
|
| 333 |
-
pill.className = 'pill ' + ({
|
| 334 |
|
| 335 |
const meta = document.getElementById('meta');
|
| 336 |
meta.innerHTML = '';
|
| 337 |
-
for (const [k, v] of Object.entries({
|
| 338 |
started_at: s.started_at, finished_at: s.finished_at, error: s.last_error,
|
| 339 |
-
...(s.last_config || {
|
| 340 |
-
}
|
| 341 |
if (v == null || v === '') continue;
|
| 342 |
const tr = document.createElement('tr');
|
| 343 |
-
tr.innerHTML = `<td><b>${
|
| 344 |
meta.appendChild(tr);
|
| 345 |
-
}
|
| 346 |
|
| 347 |
const logs = await fetch('/logs?tail=200').then(r => r.text());
|
| 348 |
document.getElementById('logs').textContent = logs || '(no logs yet)';
|
| 349 |
-
}
|
| 350 |
-
async function startRun() {
|
| 351 |
-
await fetch('/train', {
|
| 352 |
setTimeout(refresh, 500);
|
| 353 |
-
}
|
| 354 |
refresh();
|
| 355 |
setInterval(refresh, 5000);
|
| 356 |
</script>
|
|
|
|
| 297 |
<meta charset=utf-8>
|
| 298 |
<title>CERNenv Trainer</title>
|
| 299 |
<style>
|
| 300 |
+
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 2rem auto; max-width: 760px; color:#111 }
|
| 301 |
+
h1 { margin-bottom: 0 }
|
| 302 |
+
.muted { color:#666 }
|
| 303 |
+
pre { background:#0e1116; color:#e6edf3; padding:1rem; border-radius:6px; overflow-x:auto; max-height:50vh }
|
| 304 |
+
button { font-size:1rem; padding:.6rem 1rem; border-radius:6px; border:1px solid #888; background:#fff; cursor:pointer }
|
| 305 |
+
.pill { display:inline-block; padding:.1rem .5rem; border-radius:999px; background:#eef; color:#225 }
|
| 306 |
+
.ok { background:#dfd; color:#272 }
|
| 307 |
+
.fail { background:#fdd; color:#822 }
|
| 308 |
+
.run { background:#fdf6d8; color:#774 }
|
| 309 |
+
table { border-collapse:collapse; }
|
| 310 |
+
td { padding:.2rem .8rem .2rem 0; }
|
| 311 |
</style>
|
| 312 |
</head>
|
| 313 |
<body>
|
|
|
|
| 326 |
<pre id=logs>loading…</pre>
|
| 327 |
|
| 328 |
<script>
|
| 329 |
+
async function refresh() {
|
| 330 |
const s = await fetch('/status').then(r => r.json());
|
| 331 |
const pill = document.getElementById('status');
|
| 332 |
pill.textContent = s.status;
|
| 333 |
+
pill.className = 'pill ' + ({idle:'',running:'run',finished:'ok',failed:'fail'}[s.status] || '');
|
| 334 |
|
| 335 |
const meta = document.getElementById('meta');
|
| 336 |
meta.innerHTML = '';
|
| 337 |
+
for (const [k, v] of Object.entries({
|
| 338 |
started_at: s.started_at, finished_at: s.finished_at, error: s.last_error,
|
| 339 |
+
...(s.last_config || {}),
|
| 340 |
+
})) {
|
| 341 |
if (v == null || v === '') continue;
|
| 342 |
const tr = document.createElement('tr');
|
| 343 |
+
tr.innerHTML = `<td><b>${k}</b></td><td><code>${v}</code></td>`;
|
| 344 |
meta.appendChild(tr);
|
| 345 |
+
}
|
| 346 |
|
| 347 |
const logs = await fetch('/logs?tail=200').then(r => r.text());
|
| 348 |
document.getElementById('logs').textContent = logs || '(no logs yet)';
|
| 349 |
+
}
|
| 350 |
+
async function startRun() {
|
| 351 |
+
await fetch('/train', {method:'POST'});
|
| 352 |
setTimeout(refresh, 500);
|
| 353 |
+
}
|
| 354 |
refresh();
|
| 355 |
setInterval(refresh, 5000);
|
| 356 |
</script>
|