Spaces:
Sleeping
Sleeping
Commit ·
3c3a3df
1
Parent(s): cd470c1
Upload folder using huggingface_hub
Browse files- chats/2026-04-25-130826-this-session-is-being-continued-from-a-previous-c.txt +0 -0
- chats/init context 10 AM.txt +684 -684
- environment/config.py +15 -14
- training/MaterialForge_GRPO_Training.ipynb +68 -433
- training/Traning Runs/Run - I/MaterialForge_GRPO_Training.ipynb +0 -0
- training/Traning Runs/Run - I/baseline_comparison.png +0 -0
- training/Traning Runs/Run - I/loss_curve.png +0 -0
- training/Traning Runs/Run - I/reward_curve.png +0 -0
- training/Traning Runs/Run - II/MaterialForge_GRPO_Training.ipynb +490 -0
- training/Traning Runs/Run - II/baseline_comparison.png +0 -0
- training/Traning Runs/Run - II/loss_curve.png +0 -0
- training/Traning Runs/Run - II/reward_curve.png +0 -0
- training/Traning Runs/Run - III/MaterialForge_GRPO_Training.ipynb +490 -0
- training/train_env_wrapper.py +143 -198
chats/2026-04-25-130826-this-session-is-being-continued-from-a-previous-c.txt
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
chats/init context 10 AM.txt
CHANGED
|
@@ -1,685 +1,685 @@
|
|
| 1 |
-
╭─── Claude Code v2.1.112 ─────────────────────────────────────────────────────╮
|
| 2 |
-
│ │ Tips for getting │
|
| 3 |
-
│ Welcome back! │ started │
|
| 4 |
-
│ │ Run /init to create a … │
|
| 5 |
-
│ ▐▛███▜▌ │ ─────────────────────── │
|
| 6 |
-
│ ▝▜█████▛▘ │ Recent activity │
|
| 7 |
-
│ ▘▘ ▝▝ │ 2d ago /init │
|
| 8 |
-
│ │ /resume for more │
|
| 9 |
-
│ Sonnet 4.6 · API Usage Billing │ │
|
| 10 |
-
│ ~\Projects\Hackathon\Meta PyTorch OpenEnv Hackath… │ │
|
| 11 |
-
╰──────────────────────────────────────────────────────────────────────────────╯
|
| 12 |
-
|
| 13 |
-
↑ Install the PyCharm plugin from the JetBrains Marketplace: https://docs.claude.com/s/claude-code-jetbrains
|
| 14 |
-
⎿ SessionStart:startup hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 15 |
-
|
| 16 |
-
❯ Read full codebase and make sure you don't make any changes to the code just
|
| 17 |
-
read the code base.
|
| 18 |
-
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 19 |
-
|
| 20 |
-
● I'll explore the full codebase structure and read all the files.
|
| 21 |
-
|
| 22 |
-
Searched for 2 patterns (ctrl+o to expand)
|
| 23 |
-
|
| 24 |
-
● Now I'll read all the source files. Let me start with the top-level files and
|
| 25 |
-
work through each directory.
|
| 26 |
-
|
| 27 |
-
Read 8 files (ctrl+o to expand)
|
| 28 |
-
|
| 29 |
-
● Now reading all the Python source files.
|
| 30 |
-
|
| 31 |
-
Read 4 files (ctrl+o to expand)
|
| 32 |
-
|
| 33 |
-
● Now reading the environment package files.
|
| 34 |
-
|
| 35 |
-
Read 5 files (ctrl+o to expand)
|
| 36 |
-
|
| 37 |
-
● Now reading the scenarios, server, and test files.
|
| 38 |
-
|
| 39 |
-
Read 6 files (ctrl+o to expand)
|
| 40 |
-
|
| 41 |
-
● Now reading the test files, scripts, and frontend assets.
|
| 42 |
-
|
| 43 |
-
Read 5 files (ctrl+o to expand)
|
| 44 |
-
|
| 45 |
-
● Now reading the frontend static assets.
|
| 46 |
-
|
| 47 |
-
Read 5 files (ctrl+o to expand)
|
| 48 |
-
|
| 49 |
-
● Now reading the CSS files and remaining static assets.
|
| 50 |
-
|
| 51 |
-
Read 5 files (ctrl+o to expand)
|
| 52 |
-
|
| 53 |
-
● Finally, reading the egg-info metadata files.
|
| 54 |
-
|
| 55 |
-
Read 5 files (ctrl+o to expand)
|
| 56 |
-
|
| 57 |
-
● I've read the entire codebase. Here's a complete summary:
|
| 58 |
-
|
| 59 |
-
---
|
| 60 |
-
MaterialForge - Full Codebase Summary
|
| 61 |
-
|
| 62 |
-
Project: A reinforcement learning environment for autonomous crystalline
|
| 63 |
-
structure discovery, built for the Meta PyTorch OpenEnv Hackathon. Agents
|
| 64 |
-
place atoms (Metal/A, Conductor/B, Ceramic/C, Polymer/P) on an 8x8 lattice to
|
| 65 |
-
match target physical properties (Hardness, Conductivity, Thermal Resistance,
|
| 66 |
-
Elasticity).
|
| 67 |
-
|
| 68 |
-
Architecture
|
| 69 |
-
|
| 70 |
-
MaterialForge/
|
| 71 |
-
├── environment/ # Core physics engine
|
| 72 |
-
│ ├── config.py # Constants: grid size (8), atom types, costs,
|
| 73 |
-
difficulty presets
|
| 74 |
-
│ ├── lattice.py # Lattice class: 8x8 grid with place/replace/remove
|
| 75 |
-
operations
|
| 76 |
-
│ ├── physics.py # Property estimation, phase classification,
|
| 77 |
-
stability/order scoring
|
| 78 |
-
|
| 79 |
-
stability, 15% order, 10% phase
|
| 80 |
-
├── server/ # FastAPI server (OpenEnv-compliant)
|
| 81 |
-
│ ├── app.py # Entry point: creates FastAPI app, mounts static
|
| 82 |
-
files, /playground route
|
| 83 |
-
│ ├── material_forge_env_environment.py # MaterialForgeEnvironment:
|
| 84 |
-
reset/step logic, done detection
|
| 85 |
-
│ └── static/ # "Discovery Lab" dashboard (HTML/CSS/JS with
|
| 86 |
-
Chart.js)
|
| 87 |
-
├── scenarios/
|
| 88 |
-
│ └── scenarios.py # 5 named scenarios (diamond-like, conductor,
|
| 89 |
-
heat-shield, flexible-polymer, balanced-alloy)
|
| 90 |
-
├── models.py # Pydantic models: MaterialForgeAction,
|
| 91 |
-
MaterialForgeObservation
|
| 92 |
-
├── client.py # WebSocket client (EnvClient subclass)
|
| 93 |
-
├── inference.py # Agent: heuristic lookahead + LLM
|
| 94 |
-
(OpenAI-compatible) for action selection
|
| 95 |
-
├── tests/
|
| 96 |
-
│ └── benchmark.py # Analytical benchmark suite (100-trial evaluations,
|
| 97 |
-
Ollama support)
|
| 98 |
-
├── openenv.yaml # 3 tasks: basic-synthesis, diamond-like,
|
| 99 |
-
superconductor-analogue
|
| 100 |
-
├── Dockerfile # Multi-stage Docker build for HF Spaces deployment
|
| 101 |
-
└── pyproject.toml # Python 3.10+, depends on openenv-core, openai,
|
| 102 |
-
gradio
|
| 103 |
-
|
| 104 |
-
Key Components
|
| 105 |
-
|
| 106 |
-
- Physics engine (physics.py): Calculates properties from atom fractions +
|
| 107 |
-
neighbor bonding + BFS percolation (for conductivity). Classifies phase via
|
| 108 |
-
2x2 pattern repetition analysis. Computes structural stability (coordination
|
| 109 |
-
energy + mirror symmetry) and lattice order (Bragg-like + quadrant
|
| 110 |
-
homogeneity).
|
| 111 |
-
- Inference agent (inference.py): Generates candidate actions via heuristic
|
| 112 |
-
scoring (clustering, center placement), simulates each locally via the physics
|
| 113 |
-
engine, then asks an LLM to pick the best from top-8 candidates. Falls back
|
| 114 |
-
to pure heuristic if LLM fails.
|
| 115 |
-
- Frontend dashboard: Dark-themed professional UI with real-time lattice
|
| 116 |
-
canvas, property alignment bars, reward convergence chart, KPIs, and analytics
|
| 117 |
-
tab. WebSocket-connected to the server.
|
| 118 |
-
- 3 difficulty levels: Easy (tolerance 20, budget 120), Medium (tolerance 10,
|
| 119 |
-
budget 80), Hard (tolerance 5, budget 60).
|
| 120 |
-
|
| 121 |
-
No code changes were made.
|
| 122 |
-
|
| 123 |
-
● Ran 1 stop hook (ctrl+o to expand)
|
| 124 |
-
⎿ Stop hook error: Failed with non-blocking status code:
|
| 125 |
-
/usr/bin/bash: line 1: C:Program: command not found
|
| 126 |
-
|
| 127 |
-
✻ Worked for 7m 27s
|
| 128 |
-
|
| 129 |
-
❯ We have already qualified Round 1 and are now in the Grand Finale. Our
|
| 130 |
-
environment is largely complete and functional. The remaining work likely
|
| 131 |
-
involves training, final packaging, and required submissions.
|
| 132 |
-
|
| 133 |
-
Your task is to determine EXACTLY what actions are required next.
|
| 134 |
-
|
| 135 |
-
You must carefully analyze the following resources:
|
| 136 |
-
|
| 137 |
-
VIDEO
|
| 138 |
-
https://youtu.be/Jew4lhAiqnw?si=BzcMT5UkCfjtwvp-
|
| 139 |
-
|
| 140 |
-
DOCUMENTS
|
| 141 |
-
https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlEu8M9Z
|
| 142 |
-
GU/edit?tab=t.0
|
| 143 |
-
https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6m
|
| 144 |
-
Fs/preview?pru=AAABnebPvLA*TA1iaP5LykX7FUN4W27LdQ&tab=t.0#heading=h.b0qzylowp
|
| 145 |
-
ixh
|
| 146 |
-
https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11ERvgg
|
| 147 |
-
PM/edit?tab=t.0
|
| 148 |
-
https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb0wDj2
|
| 149 |
-
o4/edit?tab=t.0#heading=h.iksn3i4lpwnt
|
| 150 |
-
https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6m
|
| 151 |
-
Fs/edit?usp=drivesdk
|
| 152 |
-
|
| 153 |
-
Instructions:
|
| 154 |
-
|
| 155 |
-
1. Read every document completely.
|
| 156 |
-
2. Visit every link referenced inside those documents.
|
| 157 |
-
3. Extract all instructions, requirements, deadlines, and submission steps
|
| 158 |
-
related to the FINAL ROUND.
|
| 159 |
-
4. Watch the entire video and extract any guidance or rules mentioned by the
|
| 160 |
-
organizers.
|
| 161 |
-
5. Identify any required artifacts or deliverables (examples: environment
|
| 162 |
-
repo, training results, evaluation scripts, demos, presentations, etc.).
|
| 163 |
-
|
| 164 |
-
After reviewing everything, produce the following structured output:
|
| 165 |
-
|
| 166 |
-
SECTION 1 — Final Round Requirements
|
| 167 |
-
List all explicit requirements given by the organizers.
|
| 168 |
-
|
| 169 |
-
SECTION 2 — Required Deliverables
|
| 170 |
-
List everything the team must submit or demonstrate.
|
| 171 |
-
|
| 172 |
-
SECTION 3 — Training Requirements
|
| 173 |
-
Identify:
|
| 174 |
-
- whether RL training is expected
|
| 175 |
-
- recommended algorithms or tooling
|
| 176 |
-
- expected evaluation metrics or benchmarks
|
| 177 |
-
|
| 178 |
-
SECTION 4 — Submission Process
|
| 179 |
-
Explain:
|
| 180 |
-
- where submissions happen
|
| 181 |
-
- what files or links are needed
|
| 182 |
-
- any format requirements.
|
| 183 |
-
|
| 184 |
-
SECTION 5 — Judging Criteria
|
| 185 |
-
Extract how projects will be evaluated.
|
| 186 |
-
|
| 187 |
-
SECTION 6 — Immediate Action Plan
|
| 188 |
-
Create a prioritized checklist of the next steps our team should take to
|
| 189 |
-
maximize our chances of winning.
|
| 190 |
-
|
| 191 |
-
Focus on actionable insights only. Do not summarize the documents broadly.
|
| 192 |
-
Extract only the information relevant to what we must do next. Dont make any
|
| 193 |
-
code changes yet
|
| 194 |
-
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 195 |
-
|
| 196 |
-
● I'll fetch all the documents and video to extract the finale requirements.
|
| 197 |
-
Starting with all resources in parallel.
|
| 198 |
-
|
| 199 |
-
● Fetch(https://youtu.be/Jew4lhAiqnw?si=BzcMT5UkCfjtwvp-)
|
| 200 |
-
⎿ Error: Request failed with status code 303
|
| 201 |
-
|
| 202 |
-
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 203 |
-
u8M9ZGU/edit?tab=t.0)
|
| 204 |
-
⎿ Received 574.3KB (200 OK)
|
| 205 |
-
|
| 206 |
-
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 207 |
-
xbC6mFs/preview?pru=AAABnebPvLA*TA1iaP5LykX7FUN4W27LdQ&tab=t.0#heading=h.
|
| 208 |
-
b0qzylowpixh)
|
| 209 |
-
⎿ Received 282KB (200 OK)
|
| 210 |
-
|
| 211 |
-
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 212 |
-
ERvggPM/edit?tab=t.0)
|
| 213 |
-
⎿ Received 198.3KB (200 OK)
|
| 214 |
-
|
| 215 |
-
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 216 |
-
0wDj2o4/edit?tab=t.0#heading=h.iksn3i4lpwnt) ⎿ Received 519.4KB (200 OK)
|
| 217 |
-
|
| 218 |
-
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 219 |
-
xbC6mFs/edit?usp=drivesdk)
|
| 220 |
-
⎿ Received 358.2KB (200 OK)
|
| 221 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 222 |
-
|
| 223 |
-
● The Google Docs are behind authentication and the YouTube video redirected.
|
| 224 |
-
Let me try alternative approaches to access these resources.
|
| 225 |
-
|
| 226 |
-
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 227 |
-
u8M9ZGU/export?format=txt)
|
| 228 |
-
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 229 |
-
|
| 230 |
-
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 231 |
-
xbC6mFs/export?format=txt)
|
| 232 |
-
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 233 |
-
|
| 234 |
-
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 235 |
-
ERvggPM/export?format=txt)
|
| 236 |
-
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 237 |
-
|
| 238 |
-
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 239 |
-
0wDj2o4/export?format=txt)
|
| 240 |
-
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 241 |
-
|
| 242 |
-
● Fetch(https://www.youtube.com/watch?v=Jew4lhAiqnw)
|
| 243 |
-
⎿ Received 1.1MB (200 OK)
|
| 244 |
-
|
| 245 |
-
● Following the redirects to get the actual document content.
|
| 246 |
-
|
| 247 |
-
● Fetch(https://doc-08-8o-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 248 |
-
l3uocm525o/mesgsm24tv6fcjskeodkl8v654/1777090195000/110811587972687932588
|
| 249 |
-
/*/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlEu8M9ZGU?format=txt)
|
| 250 |
-
⎿ Error: Request failed with status code 400
|
| 251 |
-
|
| 252 |
-
● Fetch(https://doc-14-bs-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 253 |
-
l3uocm525o/udraaum8cqmoegp5o6gm5f37rs/1777090195000/107073175634533162618
|
| 254 |
-
/*/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6mFs?format=txt)
|
| 255 |
-
⎿ Error: Request failed with status code 400
|
| 256 |
-
|
| 257 |
-
● Fetch(https://doc-0k-40-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 258 |
-
l3uocm525o/igqp5g3rqgl01k2sbhph5jasd8/1777090195000/104102298771320400411
|
| 259 |
-
/*/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11ERvggPM?format=txt)
|
| 260 |
-
⎿ Error: Request failed with status code 400
|
| 261 |
-
|
| 262 |
-
● Fetch(https://doc-0k-bs-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 263 |
-
l3uocm525o/50ntp2srn6mv6e0t942chcuo4s/1777090200000/107073175634533162618
|
| 264 |
-
/*/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb0wDj2o4?format=txt)
|
| 265 |
-
⎿ Error: Request failed with status code 400
|
| 266 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 267 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 268 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 269 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 270 |
-
|
| 271 |
-
● The redirects failed due to the wildcard in the URL. Let me try the
|
| 272 |
-
published/public versions and also search for the hackathon details online.
|
| 273 |
-
|
| 274 |
-
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 275 |
-
u8M9ZGU/pub)
|
| 276 |
-
⎿ Error: Request failed with status code 401
|
| 277 |
-
|
| 278 |
-
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 279 |
-
xbC6mFs/pub)
|
| 280 |
-
⎿ Error: Request failed with status code 401
|
| 281 |
-
|
| 282 |
-
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 283 |
-
ERvggPM/pub)
|
| 284 |
-
⎿ Error: Request failed with status code 401
|
| 285 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 286 |
-
|
| 287 |
-
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 288 |
-
0wDj2o4/pub)
|
| 289 |
-
⎿ Error: Request failed with status code 401
|
| 290 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 291 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 292 |
-
● Web Search("Meta PyTorch OpenEnv Hackathon 2026 Scaler finale judging criteria
|
| 293 |
-
submission requirements")
|
| 294 |
-
⎿ Did 10 searches in 8s
|
| 295 |
-
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 296 |
-
|
| 297 |
-
● Fetch(https://dev.to/alara_iris_5d51447594b8a3/meta-pytorch-openenv-hackathon-
|
| 298 |
-
x-sst-10nd)
|
| 299 |
-
⎿ Received 72.1KB (200 OK)
|
| 300 |
-
|
| 301 |
-
● Fetch(https://www.scaler.com/school-of-technology/meta-pytorch-hackathon)
|
| 302 |
-
⎿ Received 8.5KB (200 OK)
|
| 303 |
-
|
| 304 |
-
● Fetch(https://kalinga.ai/openenv-ai-hackathon-india-2026/)
|
| 305 |
-
⎿ Received 193KB (200 OK)
|
| 306 |
-
|
| 307 |
-
● Web Search("OpenEnv hackathon round 2 finale requirements training GRPO
|
| 308 |
-
submission themes judging 2026")
|
| 309 |
-
⎿ Did 10 searches in 7s
|
| 310 |
-
|
| 311 |
-
● Fetch(https://github.com/meta-pytorch/OpenEnv)
|
| 312 |
-
⎿ Received 434.6KB (200 OK)
|
| 313 |
-
|
| 314 |
-
● Web Search(""openenv" hackathon themes "world modeling" "long horizon" judging
|
| 315 |
-
criteria scoring environment 2026")
|
| 316 |
-
⎿ Did 8 searches in 10s
|
| 317 |
-
|
| 318 |
-
● Web Search("OpenEnv hackathon "round 2" OR "finale" training pipeline GRPO
|
| 319 |
-
unsloth TRL submission requirements environment")
|
| 320 |
-
⎿ Did 6 searches in 11s
|
| 321 |
-
|
| 322 |
-
● Fetch(https://pytorch.org/event/openenv-ai-hackathon/)
|
| 323 |
-
⎿ Received 160.2KB (200 OK)
|
| 324 |
-
|
| 325 |
-
● Fetch(https://huggingface.co/docs/trl/openenv)
|
| 326 |
-
⎿ Received 27.2KB (200 OK)
|
| 327 |
-
|
| 328 |
-
● Fetch(https://deepwiki.com/meta-pytorch/OpenEnv/12-examples-and-tutorials)
|
| 329 |
-
⎿ Interrupted · What should Claude do instead?
|
| 330 |
-
|
| 331 |
-
❯ If you are unable to read via url I'll provide you the raw text
|
| 332 |
-
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 333 |
-
|
| 334 |
-
● Yes, please paste the raw text from the documents. I've gathered significant
|
| 335 |
-
context from public sources (Scaler, PyTorch.org, TRL docs, GitHub), but the
|
| 336 |
-
Google Docs are behind authentication and contain the critical finale-specific
|
| 337 |
-
details I need — especially the Themes & Judging Criteria document and the
|
| 338 |
-
Participant Help Guide.
|
| 339 |
-
|
| 340 |
-
Please paste the content from these documents:
|
| 341 |
-
|
| 342 |
-
1. Hackathon FAQs (1ysO...)
|
| 343 |
-
2. Themes & Judging Criteria (1Odz...)
|
| 344 |
-
3. OpenEnv Hackathon Resources (1m4E...)
|
| 345 |
-
4. Participant Help Guide (1lGO...)
|
| 346 |
-
|
| 347 |
-
Also, if you can share the key points from the YouTube video (Mega Lecture
|
| 348 |
-
91), that would fill the remaining gaps. I'll compile the full action plan
|
| 349 |
-
once I have the raw text.
|
| 350 |
-
|
| 351 |
-
● Ran 1 stop hook (ctrl+o to expand)
|
| 352 |
-
⎿ Stop hook error: Failed with non-blocking status code:
|
| 353 |
-
/usr/bin/bash: line 1: C:Program: command not found
|
| 354 |
-
|
| 355 |
-
❯ I have added everything in @docs\
|
| 356 |
-
⎿ Listed directory docs\
|
| 357 |
-
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 358 |
-
|
| 359 |
-
● The documents are in the repo. Let me read all of them now.
|
| 360 |
-
|
| 361 |
-
Read 6 files (ctrl+o to expand)
|
| 362 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 363 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 364 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 365 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 366 |
-
|
| 367 |
-
● The Themes & Judging Criteria doc and the FAQ are large. Let me read them in
|
| 368 |
-
parts.
|
| 369 |
-
|
| 370 |
-
Searched for 1 pattern, read 2 files, listed 1 directory (ctrl+o to expand) ⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 371 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 372 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 373 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 374 |
-
⎿ PostToolUseFailure:Bash hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 375 |
-
⎿ PostToolUseFailure:Bash hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 376 |
-
|
| 377 |
-
● Now reading the FAQ and the transcript (key sections).
|
| 378 |
-
|
| 379 |
-
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
| 380 |
-
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 381 |
-
|
| 382 |
-
● Now let me read the key portions of the transcript for hackathon-specific
|
| 383 |
-
guidance.
|
| 384 |
-
|
| 385 |
-
Searched for 1 pattern (ctrl+o to expand)
|
| 386 |
-
|
| 387 |
-
● I now have all the critical information. Let me also verify the Themes
|
| 388 |
-
document is fully captured since I read it via bash.
|
| 389 |
-
|
| 390 |
-
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
| 391 |
-
|
| 392 |
-
● I now have all the information needed. Here is the complete analysis:
|
| 393 |
-
|
| 394 |
-
---
|
| 395 |
-
SECTION 1 — Final Round Requirements
|
| 396 |
-
|
| 397 |
-
Explicit organizer requirements:
|
| 398 |
-
|
| 399 |
-
1. Use OpenEnv (latest release) — build on top of the framework, don't
|
| 400 |
-
reinvent the wheel
|
| 401 |
-
2. A working training script using Unsloth or HF TRL, ideally as a Colab
|
| 402 |
-
notebook so judges can re-run it
|
| 403 |
-
3. Evidence of actual training — at minimum, loss and reward plots from a real
|
| 404 |
-
run
|
| 405 |
-
4. A short writeup: mini-blog on HuggingFace OR < 2 minute video on YouTube,
|
| 406 |
-
OR a short slide deck
|
| 407 |
-
5. Environment hosted on Hugging Face Spaces — discoverable and runnable;
|
| 408 |
-
judges will pull the environment from the submitted URL
|
| 409 |
-
6. A README that motivates the problem, explains the environment, and shows
|
| 410 |
-
results — with links to HF Space, blog/video, and all supplementary materials
|
| 411 |
-
7. One submission per team — pick your best idea
|
| 412 |
-
8. No large video files in the HF Hub submission — use URL references instead
|
| 413 |
-
9. No changes/commits after the submission deadline — judges evaluate the
|
| 414 |
-
snapshot at deadline
|
| 415 |
-
10. It is NOT mandatory to keep the same problem statement as Round 1 — only
|
| 416 |
-
keep it if it aligns with the finale themes
|
| 417 |
-
11. Compute credits for HuggingFace will be given onsite on April 25-26 for
|
| 418 |
-
post-training
|
| 419 |
-
|
| 420 |
-
---
|
| 421 |
-
SECTION 2 — Required Deliverables
|
| 422 |
-
|
| 423 |
-
#: 1
|
| 424 |
-
Deliverable: OpenEnv-compliant environment
|
| 425 |
-
Format: HF Space (live URL)
|
| 426 |
-
Status for MaterialForge: Partially done — deployed at
|
| 427 |
-
ArshPathan/material_forge_env but needs verification with latest OpenEnv
|
| 428 |
-
────────────────────────────────────────
|
| 429 |
-
#: 2
|
| 430 |
-
Deliverable: Training script
|
| 431 |
-
Format: Colab notebook (Unsloth or TRL + GRPO)
|
| 432 |
-
Status for MaterialForge: NOT DONE — critical gap
|
| 433 |
-
────────────────────────────────────────
|
| 434 |
-
#: 3
|
| 435 |
-
Deliverable: Training evidence
|
| 436 |
-
Format: Reward curves, loss plots, before/after comparison (.png/.jpg in repo
|
| 437 |
-
+
|
| 438 |
-
README)
|
| 439 |
-
Status for MaterialForge: NOT DONE — critical gap
|
| 440 |
-
────────────────────────────────────────
|
| 441 |
-
#: 4
|
| 442 |
-
Deliverable: Mini-blog OR <2min video OR slide deck
|
| 443 |
-
Format: HF blog post / YouTube / Slides
|
| 444 |
-
Status for MaterialForge: NOT DONE
|
| 445 |
-
────────────────────────────────────────
|
| 446 |
-
#: 5
|
| 447 |
-
Deliverable: README with results
|
| 448 |
-
Format: Markdown in repo root
|
| 449 |
-
Status for MaterialForge: Exists but needs major update with training results,
|
| 450 |
-
|
| 451 |
-
plots, and links
|
| 452 |
-
────────────────────────────────────────
|
| 453 |
-
#: 6
|
| 454 |
-
Deliverable: openenv.yaml manifest
|
| 455 |
-
Format: YAML in repo root
|
| 456 |
-
Status for MaterialForge: Done
|
| 457 |
-
────────────────────────────────────────
|
| 458 |
-
#: 7
|
| 459 |
-
Deliverable: Valid client/server separation
|
| 460 |
-
Format: Code architecture
|
| 461 |
-
Status for MaterialForge: Done
|
| 462 |
-
────────────────────────────────────────
|
| 463 |
-
#: 8
|
| 464 |
-
Deliverable: 3-minute live pitch
|
| 465 |
-
Format: In-person presentation
|
| 466 |
-
Status for MaterialForge: NOT PREPARED
|
| 467 |
-
|
| 468 |
-
---
|
| 469 |
-
SECTION 3 — Training Requirements
|
| 470 |
-
|
| 471 |
-
Is RL training expected?
|
| 472 |
-
|
| 473 |
-
YES — absolutely mandatory. The documents are explicit:
|
| 474 |
-
|
| 475 |
-
▎ "The bar isn't 'training script exists.' The bar is 'training script runs
|
| 476 |
-
▎ against the environment, the agent learns, and you can show it.'"
|
| 477 |
-
|
| 478 |
-
Training evidence is worth 20% of the score directly, and the training
|
| 479 |
-
pipeline is another 10%.
|
| 480 |
-
|
| 481 |
-
Recommended stack:
|
| 482 |
-
|
| 483 |
-
- TRL (GRPOTrainer) — the primary training framework
|
| 484 |
-
- Unsloth — for memory efficiency (QLoRA/LoRA), especially on limited hardware
|
| 485 |
-
- GRPO algorithm — preferred over PPO for LLM RL; more memory-efficient, no
|
| 486 |
-
value model needed
|
| 487 |
-
- OpenEnv environment_factory pattern — wraps your environment as tool methods
|
| 488 |
-
the trainer discovers automatically
|
| 489 |
-
|
| 490 |
-
How training should work (from TRL docs):
|
| 491 |
-
|
| 492 |
-
1. Create an environment class with reset() and tool methods (e.g.,
|
| 493 |
-
place_atom(), remove_atom())
|
| 494 |
-
2. The GRPOTrainer discovers tool methods, generates completions, parses tool
|
| 495 |
-
calls, executes them, feeds results back
|
| 496 |
-
3. Reward comes from the environment after each episode
|
| 497 |
-
4. Model: Start with a capable instruct model (e.g., Qwen/Qwen3-0.6B or
|
| 498 |
-
Qwen/Qwen2.5-3B)
|
| 499 |
-
|
| 500 |
-
Expected evaluation metrics:
|
| 501 |
-
|
| 502 |
-
- Reward curves over training steps (must be plotted, labeled, saved as
|
| 503 |
-
images)
|
| 504 |
-
- Before/after behavior comparison (untrained baseline vs. trained agent)
|
| 505 |
-
- Loss plots
|
| 506 |
-
- Quantitative comparison: random agent vs. heuristic baseline vs. trained LLM
|
| 507 |
-
agent
|
| 508 |
-
|
| 509 |
-
Key warnings from organizers:
|
| 510 |
-
|
| 511 |
-
- Don't naively upcast 4-bit to 16-bit when saving LoRA — use proper merge
|
| 512 |
-
path
|
| 513 |
-
- Start with easy tasks, use curriculum learning
|
| 514 |
-
- Binary rewards (1.0 success / 0.0 fail) often work better than shaped
|
| 515 |
-
rewards for GRPO
|
| 516 |
-
- max_completion_length must be increased for long episodes (default is too
|
| 517 |
-
small)
|
| 518 |
-
- Test reward function before training — if a capable model can't beat random,
|
| 519 |
-
reward is broken
|
| 520 |
-
|
| 521 |
-
---
|
| 522 |
-
SECTION 4 — Submission Process
|
| 523 |
-
|
| 524 |
-
Where submissions happen:
|
| 525 |
-
|
| 526 |
-
- HuggingFace Spaces — the environment must be live and accessible via URL
|
| 527 |
-
- Judges pull the environment from the submitted URL to evaluate it
|
| 528 |
-
- All supplementary materials (blog, video, slides, plots) must be linked from
|
| 529 |
-
the README
|
| 530 |
-
|
| 531 |
-
What files/links are needed:
|
| 532 |
-
|
| 533 |
-
1. HF Space URL of the running environment
|
| 534 |
-
2. Colab notebook with training script (linked from README)
|
| 535 |
-
3. Plot images (.png/.jpg) committed to the repo and embedded in README
|
| 536 |
-
4. Blog/video/slides link in README
|
| 537 |
-
5. openenv.yaml manifest in repo root
|
| 538 |
-
6. README.md with: problem motivation, how env works, results, and all links
|
| 539 |
-
|
| 540 |
-
Format requirements:
|
| 541 |
-
|
| 542 |
-
- Plots: labeled axes, .png or .jpg, embedded in README with one-line captions
|
| 543 |
-
- Video: < 2 minutes
|
| 544 |
-
- No large binary files in HF Hub
|
| 545 |
-
- README readable in 3-5 minutes
|
| 546 |
-
|
| 547 |
-
---
|
| 548 |
-
SECTION 5 — Judging Criteria
|
| 549 |
-
|
| 550 |
-
Criterion: Environment Innovation
|
| 551 |
-
Weight: 40%
|
| 552 |
-
What judges evaluate: Is it novel, creative, genuinely challenging? Does it
|
| 553 |
-
teach an LLM something it can't currently do well? Underexplored domain?
|
| 554 |
-
Fresh angle? (Judges have seen too many chess/snake/grid-world clones)
|
| 555 |
-
────────────────────────────────────────
|
| 556 |
-
Criterion: Storytelling & Presentation
|
| 557 |
-
Weight: 30%
|
| 558 |
-
What judges evaluate: Clear explanation of problem, environment, and what the
|
| 559 |
-
agent learned. Engaging demo, easy to follow for non-technical audience.
|
| 560 |
-
README answers: Problem → Environment → Results → Why it matters
|
| 561 |
-
────────────────────────────────────────
|
| 562 |
-
Criterion: Showing Improvement in Rewards
|
| 563 |
-
Weight: 20%
|
| 564 |
-
What judges evaluate: Observable training progress: reward curves,
|
| 565 |
-
before/after
|
| 566 |
-
behavior, baseline comparison. Proves the agent actually learned something
|
| 567 |
-
────────────────────────────────────────
|
| 568 |
-
Criterion: Reward & Training Pipeline
|
| 569 |
-
Weight: 10%
|
| 570 |
-
What judges evaluate: Coherent reward logic, pipeline produces meaningful
|
| 571 |
-
improvement in agent behavior
|
| 572 |
-
|
| 573 |
-
What makes a submission stand out:
|
| 574 |
-
|
| 575 |
-
- Ambitious, original problem — "Does this environment exist to teach an LLM
|
| 576 |
-
something it currently can't do well?"
|
| 577 |
-
- Rich reward signal — not just 0/1, uses OpenEnv's Rubric system, hard to
|
| 578 |
-
game
|
| 579 |
-
- Real end-to-end training — not just "script exists" but "script runs, agent
|
| 580 |
-
learns, you can show it"
|
| 581 |
-
- Readable plots on same axes for comparison
|
| 582 |
-
- A story, not an API doc — problem gap → environment → results → why it
|
| 583 |
-
matters
|
| 584 |
-
|
| 585 |
-
MaterialForge's theme alignment:
|
| 586 |
-
|
| 587 |
-
MaterialForge fits Theme #3.1 (World Modeling — Professional Tasks) and Theme
|
| 588 |
-
#2 (Long-Horizon Planning). The scientific workflow (inverse molecular design)
|
| 589 |
-
is a genuinely novel domain for LLM RL training — this is a strong
|
| 590 |
-
differentiator.
|
| 591 |
-
|
| 592 |
-
---
|
| 593 |
-
SECTION 6 — Immediate Action Plan
|
| 594 |
-
|
| 595 |
-
CRITICAL PATH (must-do, in priority order):
|
| 596 |
-
|
| 597 |
-
- 1. Build the TRL/GRPO Training Script (HIGHEST PRIORITY)
|
| 598 |
-
- Create a Colab notebook that uses GRPOTrainer with environment_factory
|
| 599 |
-
- Wrap MaterialForge as a TRL-compatible environment class with tool methods
|
| 600 |
-
(place_atom, remove_atom, replace_atom) instead of a generic step()
|
| 601 |
-
- Use Unsloth for memory efficiency
|
| 602 |
-
- Start model: Qwen/Qwen3-0.6B or Qwen/Qwen2.5-3B
|
| 603 |
-
- Connect to the MaterialForge HF Space via WebSocket
|
| 604 |
-
- This unlocks 30% of the score (20% improvement evidence + 10% pipeline)
|
| 605 |
-
- 2. Run Training & Capture Evidence
|
| 606 |
-
- Execute training with at least enough steps for curves to be meaningful
|
| 607 |
-
- Save reward curves and loss plots as .png with labeled axes
|
| 608 |
-
- Record baseline comparison: random agent score vs. heuristic vs. trained
|
| 609 |
-
LLM
|
| 610 |
-
- Use the onsite HF compute credits (April 25-26) for larger runs
|
| 611 |
-
- Before/after qualitative examples (grid screenshots showing trained agent
|
| 612 |
-
building better crystals)
|
| 613 |
-
- 3. Update README with Results
|
| 614 |
-
- Restructure to tell the story: Problem → Environment → Results → Why It
|
| 615 |
-
Matters
|
| 616 |
-
- Embed training plots with one-line captions
|
| 617 |
-
- Add links to: HF Space, Colab training notebook, video/blog
|
| 618 |
-
- Show baseline vs. trained comparison table
|
| 619 |
-
- Make it readable in 3-5 minutes
|
| 620 |
-
- 4. Create Mini-Blog or <2min Video
|
| 621 |
-
- Option A: HuggingFace blog post explaining the environment and training
|
| 622 |
-
results
|
| 623 |
-
- Option B: YouTube video (<2 min) showing the Discovery Lab dashboard +
|
| 624 |
-
training progress
|
| 625 |
-
- Option C: Short slide deck for the pitch
|
| 626 |
-
- Link it from README
|
| 627 |
-
- 5. Verify HF Space is Live & Current
|
| 628 |
-
- Ensure ArshPathan/material_forge_env is running and accessible
|
| 629 |
-
- Test that judges can hit /reset and /step endpoints
|
| 630 |
-
- Verify openenv.yaml is valid with latest OpenEnv release
|
| 631 |
-
- 6. Prepare 3-Minute Pitch
|
| 632 |
-
- Structure: Problem (inverse molecular design) → Environment (what agent
|
| 633 |
-
sees/does) → Demo (show dashboard) → Training Results (reward curves,
|
| 634 |
-
before/after) → Why it matters (scientific discovery)
|
| 635 |
-
- Practice delivering in 3 minutes
|
| 636 |
-
- Emphasize the novelty: scientific professional workflow, not another game
|
| 637 |
-
clone
|
| 638 |
-
|
| 639 |
-
NICE-TO-HAVE (if time permits):
|
| 640 |
-
|
| 641 |
-
- Add curriculum learning (easy → medium → hard scenarios during training)
|
| 642 |
-
- Multiple reward function ablations to show robustness
|
| 643 |
-
- Anti-reward-hacking analysis
|
| 644 |
-
- WandB integration for richer training logs
|
| 645 |
-
|
| 646 |
-
Time allocation suggestion (for the 48-hour finale):
|
| 647 |
-
|
| 648 |
-
┌───────────┬───────┬─────────────────────────────────────────────────────┐
|
| 649 |
-
│ Phase │ Hours │ Activity │
|
| 650 |
-
├───────────┼───────┼────────────────────────────────────────────────────
|
| 651 |
-
│ Hours 0-4 │ 4h │ Build TRL training script + environment wrapper in │
|
| 652 |
-
│ │ │ Colab │
|
| 653 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 654 |
-
│ Hours 4-8 │ 4h │ Debug training loop, get first reward curves │
|
| 655 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 656 |
-
│ Hours │ 8h │ Run training with onsite compute credits, iterate │
|
| 657 |
-
│ 8-16 │ │ on rewards │
|
| 658 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 659 |
-
│ Hours │ 8h │ Capture all evidence, generate plots, run baseline │
|
| 660 |
-
│ 16-24 │ │ comparisons │
|
| 661 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 662 |
-
│ Hours │ 8h │ Update README, write blog/create video, clean up │
|
| 663 |
-
│ 24-32 │ │ repo │
|
| 664 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 665 |
-
│ Hours │ 8h │ Polish demo, prepare pitch, verify HF Space │
|
| 666 |
-
│ 32-40 │ │ │
|
| 667 |
-
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 668 |
-
│ Hours │ 8h │ Final submission prep, rehearse pitch, buffer for │
|
| 669 |
-
│ 40-48 │ │ issues │
|
| 670 |
-
└───────────┴───────┴─────────────────────────────────────────────────────┘
|
| 671 |
-
|
| 672 |
-
● User answered Claude's questions:
|
| 673 |
-
⎿ · [Bonsai] What went well or could be improved on this task? Consider
|
| 674 |
-
correctness, performance, and code quality. → Awsome performance
|
| 675 |
-
|
| 676 |
-
● User answered Claude's questions:
|
| 677 |
-
⎿ · [Bonsai] How did the model do on this task? → Great
|
| 678 |
-
|
| 679 |
-
● Thank you for the feedback!
|
| 680 |
-
|
| 681 |
-
● Ran 1 stop hook (ctrl+o to expand)
|
| 682 |
-
⎿ Stop hook error: Failed with non-blocking status code:
|
| 683 |
-
/usr/bin/bash: line 1: C:Program: command not found
|
| 684 |
-
|
| 685 |
✻ Cogitated for 4m 42s
|
|
|
|
| 1 |
+
╭─── Claude Code v2.1.112 ─────────────────────────────────────────────────────╮
|
| 2 |
+
│ │ Tips for getting │
|
| 3 |
+
│ Welcome back! │ started │
|
| 4 |
+
│ │ Run /init to create a … │
|
| 5 |
+
│ ▐▛███▜▌ │ ─────────────────────── │
|
| 6 |
+
│ ▝▜█████▛▘ │ Recent activity │
|
| 7 |
+
│ ▘▘ ▝▝ │ 2d ago /init │
|
| 8 |
+
│ │ /resume for more │
|
| 9 |
+
│ Sonnet 4.6 · API Usage Billing │ │
|
| 10 |
+
│ ~\Projects\Hackathon\Meta PyTorch OpenEnv Hackath… │ │
|
| 11 |
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
| 12 |
+
|
| 13 |
+
↑ Install the PyCharm plugin from the JetBrains Marketplace: https://docs.claude.com/s/claude-code-jetbrains
|
| 14 |
+
⎿ SessionStart:startup hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 15 |
+
|
| 16 |
+
❯ Read full codebase and make sure you don't make any changes to the code just
|
| 17 |
+
read the code base.
|
| 18 |
+
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 19 |
+
|
| 20 |
+
● I'll explore the full codebase structure and read all the files.
|
| 21 |
+
|
| 22 |
+
Searched for 2 patterns (ctrl+o to expand)
|
| 23 |
+
|
| 24 |
+
● Now I'll read all the source files. Let me start with the top-level files and
|
| 25 |
+
work through each directory.
|
| 26 |
+
|
| 27 |
+
Read 8 files (ctrl+o to expand)
|
| 28 |
+
|
| 29 |
+
● Now reading all the Python source files.
|
| 30 |
+
|
| 31 |
+
Read 4 files (ctrl+o to expand)
|
| 32 |
+
|
| 33 |
+
● Now reading the environment package files.
|
| 34 |
+
|
| 35 |
+
Read 5 files (ctrl+o to expand)
|
| 36 |
+
|
| 37 |
+
● Now reading the scenarios, server, and test files.
|
| 38 |
+
|
| 39 |
+
Read 6 files (ctrl+o to expand)
|
| 40 |
+
|
| 41 |
+
● Now reading the test files, scripts, and frontend assets.
|
| 42 |
+
|
| 43 |
+
Read 5 files (ctrl+o to expand)
|
| 44 |
+
|
| 45 |
+
● Now reading the frontend static assets.
|
| 46 |
+
|
| 47 |
+
Read 5 files (ctrl+o to expand)
|
| 48 |
+
|
| 49 |
+
● Now reading the CSS files and remaining static assets.
|
| 50 |
+
|
| 51 |
+
Read 5 files (ctrl+o to expand)
|
| 52 |
+
|
| 53 |
+
● Finally, reading the egg-info metadata files.
|
| 54 |
+
|
| 55 |
+
Read 5 files (ctrl+o to expand)
|
| 56 |
+
|
| 57 |
+
● I've read the entire codebase. Here's a complete summary:
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
MaterialForge - Full Codebase Summary
|
| 61 |
+
|
| 62 |
+
Project: A reinforcement learning environment for autonomous crystalline
|
| 63 |
+
structure discovery, built for the Meta PyTorch OpenEnv Hackathon. Agents
|
| 64 |
+
place atoms (Metal/A, Conductor/B, Ceramic/C, Polymer/P) on an 8x8 lattice to
|
| 65 |
+
match target physical properties (Hardness, Conductivity, Thermal Resistance,
|
| 66 |
+
Elasticity).
|
| 67 |
+
|
| 68 |
+
Architecture
|
| 69 |
+
|
| 70 |
+
MaterialForge/
|
| 71 |
+
├── environment/ # Core physics engine
|
| 72 |
+
│ ├── config.py # Constants: grid size (8), atom types, costs,
|
| 73 |
+
difficulty presets
|
| 74 |
+
│ ├── lattice.py # Lattice class: 8x8 grid with place/replace/remove
|
| 75 |
+
operations
|
| 76 |
+
│ ├── physics.py # Property estimation, phase classification,
|
| 77 |
+
stability/order scoring
|
| 78 |
+
�� └── rubrics.py # HeuristicRewardRubric: 50% property match, 25%
|
| 79 |
+
stability, 15% order, 10% phase
|
| 80 |
+
├── server/ # FastAPI server (OpenEnv-compliant)
|
| 81 |
+
│ ├── app.py # Entry point: creates FastAPI app, mounts static
|
| 82 |
+
files, /playground route
|
| 83 |
+
│ ├── material_forge_env_environment.py # MaterialForgeEnvironment:
|
| 84 |
+
reset/step logic, done detection
|
| 85 |
+
│ └── static/ # "Discovery Lab" dashboard (HTML/CSS/JS with
|
| 86 |
+
Chart.js)
|
| 87 |
+
├── scenarios/
|
| 88 |
+
│ └── scenarios.py # 5 named scenarios (diamond-like, conductor,
|
| 89 |
+
heat-shield, flexible-polymer, balanced-alloy)
|
| 90 |
+
├── models.py # Pydantic models: MaterialForgeAction,
|
| 91 |
+
MaterialForgeObservation
|
| 92 |
+
├── client.py # WebSocket client (EnvClient subclass)
|
| 93 |
+
├── inference.py # Agent: heuristic lookahead + LLM
|
| 94 |
+
(OpenAI-compatible) for action selection
|
| 95 |
+
├── tests/
|
| 96 |
+
│ └── benchmark.py # Analytical benchmark suite (100-trial evaluations,
|
| 97 |
+
Ollama support)
|
| 98 |
+
├── openenv.yaml # 3 tasks: basic-synthesis, diamond-like,
|
| 99 |
+
superconductor-analogue
|
| 100 |
+
├── Dockerfile # Multi-stage Docker build for HF Spaces deployment
|
| 101 |
+
└── pyproject.toml # Python 3.10+, depends on openenv-core, openai,
|
| 102 |
+
gradio
|
| 103 |
+
|
| 104 |
+
Key Components
|
| 105 |
+
|
| 106 |
+
- Physics engine (physics.py): Calculates properties from atom fractions +
|
| 107 |
+
neighbor bonding + BFS percolation (for conductivity). Classifies phase via
|
| 108 |
+
2x2 pattern repetition analysis. Computes structural stability (coordination
|
| 109 |
+
energy + mirror symmetry) and lattice order (Bragg-like + quadrant
|
| 110 |
+
homogeneity).
|
| 111 |
+
- Inference agent (inference.py): Generates candidate actions via heuristic
|
| 112 |
+
scoring (clustering, center placement), simulates each locally via the physics
|
| 113 |
+
engine, then asks an LLM to pick the best from top-8 candidates. Falls back
|
| 114 |
+
to pure heuristic if LLM fails.
|
| 115 |
+
- Frontend dashboard: Dark-themed professional UI with real-time lattice
|
| 116 |
+
canvas, property alignment bars, reward convergence chart, KPIs, and analytics
|
| 117 |
+
tab. WebSocket-connected to the server.
|
| 118 |
+
- 3 difficulty levels: Easy (tolerance 20, budget 120), Medium (tolerance 10,
|
| 119 |
+
budget 80), Hard (tolerance 5, budget 60).
|
| 120 |
+
|
| 121 |
+
No code changes were made.
|
| 122 |
+
|
| 123 |
+
● Ran 1 stop hook (ctrl+o to expand)
|
| 124 |
+
⎿ Stop hook error: Failed with non-blocking status code:
|
| 125 |
+
/usr/bin/bash: line 1: C:Program: command not found
|
| 126 |
+
|
| 127 |
+
✻ Worked for 7m 27s
|
| 128 |
+
|
| 129 |
+
❯ We have already qualified Round 1 and are now in the Grand Finale. Our
|
| 130 |
+
environment is largely complete and functional. The remaining work likely
|
| 131 |
+
involves training, final packaging, and required submissions.
|
| 132 |
+
|
| 133 |
+
Your task is to determine EXACTLY what actions are required next.
|
| 134 |
+
|
| 135 |
+
You must carefully analyze the following resources:
|
| 136 |
+
|
| 137 |
+
VIDEO
|
| 138 |
+
https://youtu.be/Jew4lhAiqnw?si=BzcMT5UkCfjtwvp-
|
| 139 |
+
|
| 140 |
+
DOCUMENTS
|
| 141 |
+
https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlEu8M9Z
|
| 142 |
+
GU/edit?tab=t.0
|
| 143 |
+
https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6m
|
| 144 |
+
Fs/preview?pru=AAABnebPvLA*TA1iaP5LykX7FUN4W27LdQ&tab=t.0#heading=h.b0qzylowp
|
| 145 |
+
ixh
|
| 146 |
+
https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11ERvgg
|
| 147 |
+
PM/edit?tab=t.0
|
| 148 |
+
https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb0wDj2
|
| 149 |
+
o4/edit?tab=t.0#heading=h.iksn3i4lpwnt
|
| 150 |
+
https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6m
|
| 151 |
+
Fs/edit?usp=drivesdk
|
| 152 |
+
|
| 153 |
+
Instructions:
|
| 154 |
+
|
| 155 |
+
1. Read every document completely.
|
| 156 |
+
2. Visit every link referenced inside those documents.
|
| 157 |
+
3. Extract all instructions, requirements, deadlines, and submission steps
|
| 158 |
+
related to the FINAL ROUND.
|
| 159 |
+
4. Watch the entire video and extract any guidance or rules mentioned by the
|
| 160 |
+
organizers.
|
| 161 |
+
5. Identify any required artifacts or deliverables (examples: environment
|
| 162 |
+
repo, training results, evaluation scripts, demos, presentations, etc.).
|
| 163 |
+
|
| 164 |
+
After reviewing everything, produce the following structured output:
|
| 165 |
+
|
| 166 |
+
SECTION 1 — Final Round Requirements
|
| 167 |
+
List all explicit requirements given by the organizers.
|
| 168 |
+
|
| 169 |
+
SECTION 2 — Required Deliverables
|
| 170 |
+
List everything the team must submit or demonstrate.
|
| 171 |
+
|
| 172 |
+
SECTION 3 — Training Requirements
|
| 173 |
+
Identify:
|
| 174 |
+
- whether RL training is expected
|
| 175 |
+
- recommended algorithms or tooling
|
| 176 |
+
- expected evaluation metrics or benchmarks
|
| 177 |
+
|
| 178 |
+
SECTION 4 — Submission Process
|
| 179 |
+
Explain:
|
| 180 |
+
- where submissions happen
|
| 181 |
+
- what files or links are needed
|
| 182 |
+
- any format requirements.
|
| 183 |
+
|
| 184 |
+
SECTION 5 — Judging Criteria
|
| 185 |
+
Extract how projects will be evaluated.
|
| 186 |
+
|
| 187 |
+
SECTION 6 — Immediate Action Plan
|
| 188 |
+
Create a prioritized checklist of the next steps our team should take to
|
| 189 |
+
maximize our chances of winning.
|
| 190 |
+
|
| 191 |
+
Focus on actionable insights only. Do not summarize the documents broadly.
|
| 192 |
+
Extract only the information relevant to what we must do next. Dont make any
|
| 193 |
+
code changes yet
|
| 194 |
+
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 195 |
+
|
| 196 |
+
● I'll fetch all the documents and video to extract the finale requirements.
|
| 197 |
+
Starting with all resources in parallel.
|
| 198 |
+
|
| 199 |
+
● Fetch(https://youtu.be/Jew4lhAiqnw?si=BzcMT5UkCfjtwvp-)
|
| 200 |
+
⎿ Error: Request failed with status code 303
|
| 201 |
+
|
| 202 |
+
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 203 |
+
u8M9ZGU/edit?tab=t.0)
|
| 204 |
+
⎿ Received 574.3KB (200 OK)
|
| 205 |
+
|
| 206 |
+
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 207 |
+
xbC6mFs/preview?pru=AAABnebPvLA*TA1iaP5LykX7FUN4W27LdQ&tab=t.0#heading=h.
|
| 208 |
+
b0qzylowpixh)
|
| 209 |
+
⎿ Received 282KB (200 OK)
|
| 210 |
+
|
| 211 |
+
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 212 |
+
ERvggPM/edit?tab=t.0)
|
| 213 |
+
⎿ Received 198.3KB (200 OK)
|
| 214 |
+
|
| 215 |
+
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 216 |
+
0wDj2o4/edit?tab=t.0#heading=h.iksn3i4lpwnt) ⎿ Received 519.4KB (200 OK)
|
| 217 |
+
|
| 218 |
+
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 219 |
+
xbC6mFs/edit?usp=drivesdk)
|
| 220 |
+
⎿ Received 358.2KB (200 OK)
|
| 221 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 222 |
+
|
| 223 |
+
● The Google Docs are behind authentication and the YouTube video redirected.
|
| 224 |
+
Let me try alternative approaches to access these resources.
|
| 225 |
+
|
| 226 |
+
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 227 |
+
u8M9ZGU/export?format=txt)
|
| 228 |
+
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 229 |
+
|
| 230 |
+
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 231 |
+
xbC6mFs/export?format=txt)
|
| 232 |
+
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 233 |
+
|
| 234 |
+
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 235 |
+
ERvggPM/export?format=txt)
|
| 236 |
+
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 237 |
+
|
| 238 |
+
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 239 |
+
0wDj2o4/export?format=txt)
|
| 240 |
+
⎿ Received 820 bytes (307 Temporary Redirect)
|
| 241 |
+
|
| 242 |
+
● Fetch(https://www.youtube.com/watch?v=Jew4lhAiqnw)
|
| 243 |
+
⎿ Received 1.1MB (200 OK)
|
| 244 |
+
|
| 245 |
+
● Following the redirects to get the actual document content.
|
| 246 |
+
|
| 247 |
+
● Fetch(https://doc-08-8o-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 248 |
+
l3uocm525o/mesgsm24tv6fcjskeodkl8v654/1777090195000/110811587972687932588
|
| 249 |
+
/*/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlEu8M9ZGU?format=txt)
|
| 250 |
+
⎿ Error: Request failed with status code 400
|
| 251 |
+
|
| 252 |
+
● Fetch(https://doc-14-bs-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 253 |
+
l3uocm525o/udraaum8cqmoegp5o6gm5f37rs/1777090195000/107073175634533162618
|
| 254 |
+
/*/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGcxbC6mFs?format=txt)
|
| 255 |
+
⎿ Error: Request failed with status code 400
|
| 256 |
+
|
| 257 |
+
● Fetch(https://doc-0k-40-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 258 |
+
l3uocm525o/igqp5g3rqgl01k2sbhph5jasd8/1777090195000/104102298771320400411
|
| 259 |
+
/*/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11ERvggPM?format=txt)
|
| 260 |
+
⎿ Error: Request failed with status code 400
|
| 261 |
+
|
| 262 |
+
● Fetch(https://doc-0k-bs-docstext.googleusercontent.com/export/e6hpso97lrhpva19
|
| 263 |
+
l3uocm525o/50ntp2srn6mv6e0t942chcuo4s/1777090200000/107073175634533162618
|
| 264 |
+
/*/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb0wDj2o4?format=txt)
|
| 265 |
+
⎿ Error: Request failed with status code 400
|
| 266 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 267 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 268 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 269 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 270 |
+
|
| 271 |
+
● The redirects failed due to the wildcard in the URL. Let me try the
|
| 272 |
+
published/public versions and also search for the hackathon details online.
|
| 273 |
+
|
| 274 |
+
● Fetch(https://docs.google.com/document/d/1ysOVuq6StZpJ7mcyNi4bpmxxB1AnAWE9YGlE
|
| 275 |
+
u8M9ZGU/pub)
|
| 276 |
+
⎿ Error: Request failed with status code 401
|
| 277 |
+
|
| 278 |
+
● Fetch(https://docs.google.com/document/d/1Odznuzwtb1ecDOm2t6ToZd4MuMXXfO6vWUGc
|
| 279 |
+
xbC6mFs/pub)
|
| 280 |
+
⎿ Error: Request failed with status code 401
|
| 281 |
+
|
| 282 |
+
● Fetch(https://docs.google.com/document/d/1m4EEBkomp3UJ4Wc-ydRjITDfdzLWPyFqNx11
|
| 283 |
+
ERvggPM/pub)
|
| 284 |
+
⎿ Error: Request failed with status code 401
|
| 285 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 286 |
+
|
| 287 |
+
● Fetch(https://docs.google.com/document/d/1lGOb5w8LGSbYd7_L-L-zAD9moThrMQZpUyIb
|
| 288 |
+
0wDj2o4/pub)
|
| 289 |
+
⎿ Error: Request failed with status code 401
|
| 290 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 291 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 292 |
+
● Web Search("Meta PyTorch OpenEnv Hackathon 2026 Scaler finale judging criteria
|
| 293 |
+
submission requirements")
|
| 294 |
+
⎿ Did 10 searches in 8s
|
| 295 |
+
⎿ PostToolUseFailure:WebFetch hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 296 |
+
|
| 297 |
+
● Fetch(https://dev.to/alara_iris_5d51447594b8a3/meta-pytorch-openenv-hackathon-
|
| 298 |
+
x-sst-10nd)
|
| 299 |
+
⎿ Received 72.1KB (200 OK)
|
| 300 |
+
|
| 301 |
+
● Fetch(https://www.scaler.com/school-of-technology/meta-pytorch-hackathon)
|
| 302 |
+
⎿ Received 8.5KB (200 OK)
|
| 303 |
+
|
| 304 |
+
● Fetch(https://kalinga.ai/openenv-ai-hackathon-india-2026/)
|
| 305 |
+
⎿ Received 193KB (200 OK)
|
| 306 |
+
|
| 307 |
+
● Web Search("OpenEnv hackathon round 2 finale requirements training GRPO
|
| 308 |
+
submission themes judging 2026")
|
| 309 |
+
⎿ Did 10 searches in 7s
|
| 310 |
+
|
| 311 |
+
● Fetch(https://github.com/meta-pytorch/OpenEnv)
|
| 312 |
+
⎿ Received 434.6KB (200 OK)
|
| 313 |
+
|
| 314 |
+
● Web Search(""openenv" hackathon themes "world modeling" "long horizon" judging
|
| 315 |
+
criteria scoring environment 2026")
|
| 316 |
+
⎿ Did 8 searches in 10s
|
| 317 |
+
|
| 318 |
+
● Web Search("OpenEnv hackathon "round 2" OR "finale" training pipeline GRPO
|
| 319 |
+
unsloth TRL submission requirements environment")
|
| 320 |
+
⎿ Did 6 searches in 11s
|
| 321 |
+
|
| 322 |
+
● Fetch(https://pytorch.org/event/openenv-ai-hackathon/)
|
| 323 |
+
⎿ Received 160.2KB (200 OK)
|
| 324 |
+
|
| 325 |
+
● Fetch(https://huggingface.co/docs/trl/openenv)
|
| 326 |
+
⎿ Received 27.2KB (200 OK)
|
| 327 |
+
|
| 328 |
+
● Fetch(https://deepwiki.com/meta-pytorch/OpenEnv/12-examples-and-tutorials)
|
| 329 |
+
⎿ Interrupted · What should Claude do instead?
|
| 330 |
+
|
| 331 |
+
❯ If you are unable to read via url I'll provide you the raw text
|
| 332 |
+
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 333 |
+
|
| 334 |
+
● Yes, please paste the raw text from the documents. I've gathered significant
|
| 335 |
+
context from public sources (Scaler, PyTorch.org, TRL docs, GitHub), but the
|
| 336 |
+
Google Docs are behind authentication and contain the critical finale-specific
|
| 337 |
+
details I need — especially the Themes & Judging Criteria document and the
|
| 338 |
+
Participant Help Guide.
|
| 339 |
+
|
| 340 |
+
Please paste the content from these documents:
|
| 341 |
+
|
| 342 |
+
1. Hackathon FAQs (1ysO...)
|
| 343 |
+
2. Themes & Judging Criteria (1Odz...)
|
| 344 |
+
3. OpenEnv Hackathon Resources (1m4E...)
|
| 345 |
+
4. Participant Help Guide (1lGO...)
|
| 346 |
+
|
| 347 |
+
Also, if you can share the key points from the YouTube video (Mega Lecture
|
| 348 |
+
91), that would fill the remaining gaps. I'll compile the full action plan
|
| 349 |
+
once I have the raw text.
|
| 350 |
+
|
| 351 |
+
● Ran 1 stop hook (ctrl+o to expand)
|
| 352 |
+
⎿ Stop hook error: Failed with non-blocking status code:
|
| 353 |
+
/usr/bin/bash: line 1: C:Program: command not found
|
| 354 |
+
|
| 355 |
+
❯ I have added everything in @docs\
|
| 356 |
+
⎿ Listed directory docs\
|
| 357 |
+
⎿ UserPromptSubmit hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 358 |
+
|
| 359 |
+
● The documents are in the repo. Let me read all of them now.
|
| 360 |
+
|
| 361 |
+
Read 6 files (ctrl+o to expand)
|
| 362 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 363 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 364 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 365 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1:
|
| 366 |
+
|
| 367 |
+
● The Themes & Judging Criteria doc and the FAQ are large. Let me read them in
|
| 368 |
+
parts.
|
| 369 |
+
|
| 370 |
+
Searched for 1 pattern, read 2 files, listed 1 directory (ctrl+o to expand) ⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 371 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 372 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 373 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 374 |
+
⎿ PostToolUseFailure:Bash hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 375 |
+
⎿ PostToolUseFailure:Bash hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 376 |
+
|
| 377 |
+
● Now reading the FAQ and the transcript (key sections).
|
| 378 |
+
|
| 379 |
+
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
| 380 |
+
⎿ PostToolUseFailure:Read hook error ⎿ Failed with non-blocking status code: /usr/bin/bash: line 1: C:Program: command not found
|
| 381 |
+
|
| 382 |
+
● Now let me read the key portions of the transcript for hackathon-specific
|
| 383 |
+
guidance.
|
| 384 |
+
|
| 385 |
+
Searched for 1 pattern (ctrl+o to expand)
|
| 386 |
+
|
| 387 |
+
● I now have all the critical information. Let me also verify the Themes
|
| 388 |
+
document is fully captured since I read it via bash.
|
| 389 |
+
|
| 390 |
+
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
| 391 |
+
|
| 392 |
+
● I now have all the information needed. Here is the complete analysis:
|
| 393 |
+
|
| 394 |
+
---
|
| 395 |
+
SECTION 1 — Final Round Requirements
|
| 396 |
+
|
| 397 |
+
Explicit organizer requirements:
|
| 398 |
+
|
| 399 |
+
1. Use OpenEnv (latest release) — build on top of the framework, don't
|
| 400 |
+
reinvent the wheel
|
| 401 |
+
2. A working training script using Unsloth or HF TRL, ideally as a Colab
|
| 402 |
+
notebook so judges can re-run it
|
| 403 |
+
3. Evidence of actual training — at minimum, loss and reward plots from a real
|
| 404 |
+
run
|
| 405 |
+
4. A short writeup: mini-blog on HuggingFace OR < 2 minute video on YouTube,
|
| 406 |
+
OR a short slide deck
|
| 407 |
+
5. Environment hosted on Hugging Face Spaces — discoverable and runnable;
|
| 408 |
+
judges will pull the environment from the submitted URL
|
| 409 |
+
6. A README that motivates the problem, explains the environment, and shows
|
| 410 |
+
results — with links to HF Space, blog/video, and all supplementary materials
|
| 411 |
+
7. One submission per team — pick your best idea
|
| 412 |
+
8. No large video files in the HF Hub submission — use URL references instead
|
| 413 |
+
9. No changes/commits after the submission deadline — judges evaluate the
|
| 414 |
+
snapshot at deadline
|
| 415 |
+
10. It is NOT mandatory to keep the same problem statement as Round 1 — only
|
| 416 |
+
keep it if it aligns with the finale themes
|
| 417 |
+
11. Compute credits for HuggingFace will be given onsite on April 25-26 for
|
| 418 |
+
post-training
|
| 419 |
+
|
| 420 |
+
---
|
| 421 |
+
SECTION 2 — Required Deliverables
|
| 422 |
+
|
| 423 |
+
#: 1
|
| 424 |
+
Deliverable: OpenEnv-compliant environment
|
| 425 |
+
Format: HF Space (live URL)
|
| 426 |
+
Status for MaterialForge: Partially done — deployed at
|
| 427 |
+
ArshPathan/material_forge_env but needs verification with latest OpenEnv
|
| 428 |
+
────────────────────────────────────────
|
| 429 |
+
#: 2
|
| 430 |
+
Deliverable: Training script
|
| 431 |
+
Format: Colab notebook (Unsloth or TRL + GRPO)
|
| 432 |
+
Status for MaterialForge: NOT DONE — critical gap
|
| 433 |
+
────────────────────────────────────────
|
| 434 |
+
#: 3
|
| 435 |
+
Deliverable: Training evidence
|
| 436 |
+
Format: Reward curves, loss plots, before/after comparison (.png/.jpg in repo
|
| 437 |
+
+
|
| 438 |
+
README)
|
| 439 |
+
Status for MaterialForge: NOT DONE — critical gap
|
| 440 |
+
────────────────────────────────────────
|
| 441 |
+
#: 4
|
| 442 |
+
Deliverable: Mini-blog OR <2min video OR slide deck
|
| 443 |
+
Format: HF blog post / YouTube / Slides
|
| 444 |
+
Status for MaterialForge: NOT DONE
|
| 445 |
+
────────────────────────────────────────
|
| 446 |
+
#: 5
|
| 447 |
+
Deliverable: README with results
|
| 448 |
+
Format: Markdown in repo root
|
| 449 |
+
Status for MaterialForge: Exists but needs major update with training results,
|
| 450 |
+
|
| 451 |
+
plots, and links
|
| 452 |
+
────────────────────────────────────────
|
| 453 |
+
#: 6
|
| 454 |
+
Deliverable: openenv.yaml manifest
|
| 455 |
+
Format: YAML in repo root
|
| 456 |
+
Status for MaterialForge: Done
|
| 457 |
+
────────────────────────────────────────
|
| 458 |
+
#: 7
|
| 459 |
+
Deliverable: Valid client/server separation
|
| 460 |
+
Format: Code architecture
|
| 461 |
+
Status for MaterialForge: Done
|
| 462 |
+
────────────────────────────────────────
|
| 463 |
+
#: 8
|
| 464 |
+
Deliverable: 3-minute live pitch
|
| 465 |
+
Format: In-person presentation
|
| 466 |
+
Status for MaterialForge: NOT PREPARED
|
| 467 |
+
|
| 468 |
+
---
|
| 469 |
+
SECTION 3 — Training Requirements
|
| 470 |
+
|
| 471 |
+
Is RL training expected?
|
| 472 |
+
|
| 473 |
+
YES — absolutely mandatory. The documents are explicit:
|
| 474 |
+
|
| 475 |
+
▎ "The bar isn't 'training script exists.' The bar is 'training script runs
|
| 476 |
+
▎ against the environment, the agent learns, and you can show it.'"
|
| 477 |
+
|
| 478 |
+
Training evidence is worth 20% of the score directly, and the training
|
| 479 |
+
pipeline is another 10%.
|
| 480 |
+
|
| 481 |
+
Recommended stack:
|
| 482 |
+
|
| 483 |
+
- TRL (GRPOTrainer) — the primary training framework
|
| 484 |
+
- Unsloth — for memory efficiency (QLoRA/LoRA), especially on limited hardware
|
| 485 |
+
- GRPO algorithm — preferred over PPO for LLM RL; more memory-efficient, no
|
| 486 |
+
value model needed
|
| 487 |
+
- OpenEnv environment_factory pattern — wraps your environment as tool methods
|
| 488 |
+
the trainer discovers automatically
|
| 489 |
+
|
| 490 |
+
How training should work (from TRL docs):
|
| 491 |
+
|
| 492 |
+
1. Create an environment class with reset() and tool methods (e.g.,
|
| 493 |
+
place_atom(), remove_atom())
|
| 494 |
+
2. The GRPOTrainer discovers tool methods, generates completions, parses tool
|
| 495 |
+
calls, executes them, feeds results back
|
| 496 |
+
3. Reward comes from the environment after each episode
|
| 497 |
+
4. Model: Start with a capable instruct model (e.g., Qwen/Qwen3-0.6B or
|
| 498 |
+
Qwen/Qwen2.5-3B)
|
| 499 |
+
|
| 500 |
+
Expected evaluation metrics:
|
| 501 |
+
|
| 502 |
+
- Reward curves over training steps (must be plotted, labeled, saved as
|
| 503 |
+
images)
|
| 504 |
+
- Before/after behavior comparison (untrained baseline vs. trained agent)
|
| 505 |
+
- Loss plots
|
| 506 |
+
- Quantitative comparison: random agent vs. heuristic baseline vs. trained LLM
|
| 507 |
+
agent
|
| 508 |
+
|
| 509 |
+
Key warnings from organizers:
|
| 510 |
+
|
| 511 |
+
- Don't naively upcast 4-bit to 16-bit when saving LoRA — use proper merge
|
| 512 |
+
path
|
| 513 |
+
- Start with easy tasks, use curriculum learning
|
| 514 |
+
- Binary rewards (1.0 success / 0.0 fail) often work better than shaped
|
| 515 |
+
rewards for GRPO
|
| 516 |
+
- max_completion_length must be increased for long episodes (default is too
|
| 517 |
+
small)
|
| 518 |
+
- Test reward function before training — if a capable model can't beat random,
|
| 519 |
+
reward is broken
|
| 520 |
+
|
| 521 |
+
---
|
| 522 |
+
SECTION 4 — Submission Process
|
| 523 |
+
|
| 524 |
+
Where submissions happen:
|
| 525 |
+
|
| 526 |
+
- HuggingFace Spaces — the environment must be live and accessible via URL
|
| 527 |
+
- Judges pull the environment from the submitted URL to evaluate it
|
| 528 |
+
- All supplementary materials (blog, video, slides, plots) must be linked from
|
| 529 |
+
the README
|
| 530 |
+
|
| 531 |
+
What files/links are needed:
|
| 532 |
+
|
| 533 |
+
1. HF Space URL of the running environment
|
| 534 |
+
2. Colab notebook with training script (linked from README)
|
| 535 |
+
3. Plot images (.png/.jpg) committed to the repo and embedded in README
|
| 536 |
+
4. Blog/video/slides link in README
|
| 537 |
+
5. openenv.yaml manifest in repo root
|
| 538 |
+
6. README.md with: problem motivation, how env works, results, and all links
|
| 539 |
+
|
| 540 |
+
Format requirements:
|
| 541 |
+
|
| 542 |
+
- Plots: labeled axes, .png or .jpg, embedded in README with one-line captions
|
| 543 |
+
- Video: < 2 minutes
|
| 544 |
+
- No large binary files in HF Hub
|
| 545 |
+
- README readable in 3-5 minutes
|
| 546 |
+
|
| 547 |
+
---
|
| 548 |
+
SECTION 5 — Judging Criteria
|
| 549 |
+
|
| 550 |
+
Criterion: Environment Innovation
|
| 551 |
+
Weight: 40%
|
| 552 |
+
What judges evaluate: Is it novel, creative, genuinely challenging? Does it
|
| 553 |
+
teach an LLM something it can't currently do well? Underexplored domain?
|
| 554 |
+
Fresh angle? (Judges have seen too many chess/snake/grid-world clones)
|
| 555 |
+
────────────────────────────────────────
|
| 556 |
+
Criterion: Storytelling & Presentation
|
| 557 |
+
Weight: 30%
|
| 558 |
+
What judges evaluate: Clear explanation of problem, environment, and what the
|
| 559 |
+
agent learned. Engaging demo, easy to follow for non-technical audience.
|
| 560 |
+
README answers: Problem → Environment → Results → Why it matters
|
| 561 |
+
────────────────────────────────────────
|
| 562 |
+
Criterion: Showing Improvement in Rewards
|
| 563 |
+
Weight: 20%
|
| 564 |
+
What judges evaluate: Observable training progress: reward curves,
|
| 565 |
+
before/after
|
| 566 |
+
behavior, baseline comparison. Proves the agent actually learned something
|
| 567 |
+
────────────────────────────────────────
|
| 568 |
+
Criterion: Reward & Training Pipeline
|
| 569 |
+
Weight: 10%
|
| 570 |
+
What judges evaluate: Coherent reward logic, pipeline produces meaningful
|
| 571 |
+
improvement in agent behavior
|
| 572 |
+
|
| 573 |
+
What makes a submission stand out:
|
| 574 |
+
|
| 575 |
+
- Ambitious, original problem — "Does this environment exist to teach an LLM
|
| 576 |
+
something it currently can't do well?"
|
| 577 |
+
- Rich reward signal — not just 0/1, uses OpenEnv's Rubric system, hard to
|
| 578 |
+
game
|
| 579 |
+
- Real end-to-end training — not just "script exists" but "script runs, agent
|
| 580 |
+
learns, you can show it"
|
| 581 |
+
- Readable plots on same axes for comparison
|
| 582 |
+
- A story, not an API doc — problem gap → environment → results → why it
|
| 583 |
+
matters
|
| 584 |
+
|
| 585 |
+
MaterialForge's theme alignment:
|
| 586 |
+
|
| 587 |
+
MaterialForge fits Theme #3.1 (World Modeling — Professional Tasks) and Theme
|
| 588 |
+
#2 (Long-Horizon Planning). The scientific workflow (inverse molecular design)
|
| 589 |
+
is a genuinely novel domain for LLM RL training — this is a strong
|
| 590 |
+
differentiator.
|
| 591 |
+
|
| 592 |
+
---
|
| 593 |
+
SECTION 6 — Immediate Action Plan
|
| 594 |
+
|
| 595 |
+
CRITICAL PATH (must-do, in priority order):
|
| 596 |
+
|
| 597 |
+
- 1. Build the TRL/GRPO Training Script (HIGHEST PRIORITY)
|
| 598 |
+
- Create a Colab notebook that uses GRPOTrainer with environment_factory
|
| 599 |
+
- Wrap MaterialForge as a TRL-compatible environment class with tool methods
|
| 600 |
+
(place_atom, remove_atom, replace_atom) instead of a generic step()
|
| 601 |
+
- Use Unsloth for memory efficiency
|
| 602 |
+
- Start model: Qwen/Qwen3-0.6B or Qwen/Qwen2.5-3B
|
| 603 |
+
- Connect to the MaterialForge HF Space via WebSocket
|
| 604 |
+
- This unlocks 30% of the score (20% improvement evidence + 10% pipeline)
|
| 605 |
+
- 2. Run Training & Capture Evidence
|
| 606 |
+
- Execute training with at least enough steps for curves to be meaningful
|
| 607 |
+
- Save reward curves and loss plots as .png with labeled axes
|
| 608 |
+
- Record baseline comparison: random agent score vs. heuristic vs. trained
|
| 609 |
+
LLM
|
| 610 |
+
- Use the onsite HF compute credits (April 25-26) for larger runs
|
| 611 |
+
- Before/after qualitative examples (grid screenshots showing trained agent
|
| 612 |
+
building better crystals)
|
| 613 |
+
- 3. Update README with Results
|
| 614 |
+
- Restructure to tell the story: Problem → Environment → Results → Why It
|
| 615 |
+
Matters
|
| 616 |
+
- Embed training plots with one-line captions
|
| 617 |
+
- Add links to: HF Space, Colab training notebook, video/blog
|
| 618 |
+
- Show baseline vs. trained comparison table
|
| 619 |
+
- Make it readable in 3-5 minutes
|
| 620 |
+
- 4. Create Mini-Blog or <2min Video
|
| 621 |
+
- Option A: HuggingFace blog post explaining the environment and training
|
| 622 |
+
results
|
| 623 |
+
- Option B: YouTube video (<2 min) showing the Discovery Lab dashboard +
|
| 624 |
+
training progress
|
| 625 |
+
- Option C: Short slide deck for the pitch
|
| 626 |
+
- Link it from README
|
| 627 |
+
- 5. Verify HF Space is Live & Current
|
| 628 |
+
- Ensure ArshPathan/material_forge_env is running and accessible
|
| 629 |
+
- Test that judges can hit /reset and /step endpoints
|
| 630 |
+
- Verify openenv.yaml is valid with latest OpenEnv release
|
| 631 |
+
- 6. Prepare 3-Minute Pitch
|
| 632 |
+
- Structure: Problem (inverse molecular design) → Environment (what agent
|
| 633 |
+
sees/does) → Demo (show dashboard) → Training Results (reward curves,
|
| 634 |
+
before/after) → Why it matters (scientific discovery)
|
| 635 |
+
- Practice delivering in 3 minutes
|
| 636 |
+
- Emphasize the novelty: scientific professional workflow, not another game
|
| 637 |
+
clone
|
| 638 |
+
|
| 639 |
+
NICE-TO-HAVE (if time permits):
|
| 640 |
+
|
| 641 |
+
- Add curriculum learning (easy → medium → hard scenarios during training)
|
| 642 |
+
- Multiple reward function ablations to show robustness
|
| 643 |
+
- Anti-reward-hacking analysis
|
| 644 |
+
- WandB integration for richer training logs
|
| 645 |
+
|
| 646 |
+
Time allocation suggestion (for the 48-hour finale):
|
| 647 |
+
|
| 648 |
+
┌───────────┬───────┬─────────────────────────────────────────────────────┐
|
| 649 |
+
│ Phase │ Hours │ Activity │
|
| 650 |
+
├───────────┼───────┼────��────────────────────────────────────────────────┤
|
| 651 |
+
│ Hours 0-4 │ 4h │ Build TRL training script + environment wrapper in │
|
| 652 |
+
│ │ │ Colab │
|
| 653 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 654 |
+
│ Hours 4-8 │ 4h │ Debug training loop, get first reward curves │
|
| 655 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 656 |
+
│ Hours │ 8h │ Run training with onsite compute credits, iterate │
|
| 657 |
+
│ 8-16 │ │ on rewards │
|
| 658 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 659 |
+
│ Hours │ 8h │ Capture all evidence, generate plots, run baseline │
|
| 660 |
+
│ 16-24 │ │ comparisons │
|
| 661 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 662 |
+
│ Hours │ 8h │ Update README, write blog/create video, clean up │
|
| 663 |
+
│ 24-32 │ │ repo │
|
| 664 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 665 |
+
│ Hours │ 8h │ Polish demo, prepare pitch, verify HF Space │
|
| 666 |
+
│ 32-40 │ │ │
|
| 667 |
+
├───────────┼───────┼─────────────────────────────────────────────────────┤
|
| 668 |
+
│ Hours │ 8h │ Final submission prep, rehearse pitch, buffer for │
|
| 669 |
+
│ 40-48 │ │ issues │
|
| 670 |
+
└───────────┴───────┴─────────────────────────────────────────────────────┘
|
| 671 |
+
|
| 672 |
+
● User answered Claude's questions:
|
| 673 |
+
⎿ · [Bonsai] What went well or could be improved on this task? Consider
|
| 674 |
+
correctness, performance, and code quality. → Awsome performance
|
| 675 |
+
|
| 676 |
+
● User answered Claude's questions:
|
| 677 |
+
⎿ · [Bonsai] How did the model do on this task? → Great
|
| 678 |
+
|
| 679 |
+
● Thank you for the feedback!
|
| 680 |
+
|
| 681 |
+
● Ran 1 stop hook (ctrl+o to expand)
|
| 682 |
+
⎿ Stop hook error: Failed with non-blocking status code:
|
| 683 |
+
/usr/bin/bash: line 1: C:Program: command not found
|
| 684 |
+
|
| 685 |
✻ Cogitated for 4m 42s
|
environment/config.py
CHANGED
|
@@ -14,34 +14,35 @@ VALID_COMPLETION_PHASES = {"crystalline", "polycrystalline"}
|
|
| 14 |
PROPERTY_NAMES = ["hardness", "conductivity", "thermal_resistance", "elasticity"]
|
| 15 |
|
| 16 |
# Atom definitions: defines species symbols, their resource costs, and physical contributions.
|
|
|
|
| 17 |
ATOM_TYPES = {
|
| 18 |
"A": {
|
| 19 |
"name": "metal",
|
| 20 |
"cost": 8,
|
| 21 |
"contributions": {
|
| 22 |
-
"hardness": 1.00,
|
| 23 |
-
"conductivity": 0.
|
| 24 |
-
"thermal_resistance": 0.
|
| 25 |
-
"elasticity": 0.
|
| 26 |
},
|
| 27 |
},
|
| 28 |
"B": {
|
| 29 |
"name": "conductor",
|
| 30 |
"cost": 6,
|
| 31 |
"contributions": {
|
| 32 |
-
"hardness": 0.
|
| 33 |
-
"conductivity": 1.00,
|
| 34 |
-
"thermal_resistance": 0.
|
| 35 |
-
"elasticity": 0.
|
| 36 |
},
|
| 37 |
},
|
| 38 |
"C": {
|
| 39 |
"name": "ceramic",
|
| 40 |
"cost": 4,
|
| 41 |
"contributions": {
|
| 42 |
-
"hardness": 0.
|
| 43 |
"conductivity": 0.05,
|
| 44 |
-
"thermal_resistance": 1.00,
|
| 45 |
"elasticity": 0.05,
|
| 46 |
},
|
| 47 |
},
|
|
@@ -49,10 +50,10 @@ ATOM_TYPES = {
|
|
| 49 |
"name": "polymer",
|
| 50 |
"cost": 2,
|
| 51 |
"contributions": {
|
| 52 |
-
"hardness": 0.
|
| 53 |
-
"conductivity": 0.
|
| 54 |
-
"thermal_resistance": 0.
|
| 55 |
-
"elasticity": 1.00,
|
| 56 |
},
|
| 57 |
},
|
| 58 |
}
|
|
|
|
| 14 |
PROPERTY_NAMES = ["hardness", "conductivity", "thermal_resistance", "elasticity"]
|
| 15 |
|
| 16 |
# Atom definitions: defines species symbols, their resource costs, and physical contributions.
|
| 17 |
+
# Each species is a SPECIALIST in one physical property to force chemical diversity.
|
| 18 |
ATOM_TYPES = {
|
| 19 |
"A": {
|
| 20 |
"name": "metal",
|
| 21 |
"cost": 8,
|
| 22 |
"contributions": {
|
| 23 |
+
"hardness": 1.00, # Master of Hardness
|
| 24 |
+
"conductivity": 0.15,
|
| 25 |
+
"thermal_resistance": 0.10,
|
| 26 |
+
"elasticity": 0.05,
|
| 27 |
},
|
| 28 |
},
|
| 29 |
"B": {
|
| 30 |
"name": "conductor",
|
| 31 |
"cost": 6,
|
| 32 |
"contributions": {
|
| 33 |
+
"hardness": 0.10,
|
| 34 |
+
"conductivity": 1.00, # Master of Conductivity
|
| 35 |
+
"thermal_resistance": 0.10,
|
| 36 |
+
"elasticity": 0.20,
|
| 37 |
},
|
| 38 |
},
|
| 39 |
"C": {
|
| 40 |
"name": "ceramic",
|
| 41 |
"cost": 4,
|
| 42 |
"contributions": {
|
| 43 |
+
"hardness": 0.30,
|
| 44 |
"conductivity": 0.05,
|
| 45 |
+
"thermal_resistance": 1.00, # Master of Thermal Resistance
|
| 46 |
"elasticity": 0.05,
|
| 47 |
},
|
| 48 |
},
|
|
|
|
| 50 |
"name": "polymer",
|
| 51 |
"cost": 2,
|
| 52 |
"contributions": {
|
| 53 |
+
"hardness": 0.05,
|
| 54 |
+
"conductivity": 0.15,
|
| 55 |
+
"thermal_resistance": 0.15,
|
| 56 |
+
"elasticity": 1.00, # Master of Elasticity
|
| 57 |
},
|
| 58 |
},
|
| 59 |
}
|
training/MaterialForge_GRPO_Training.ipynb
CHANGED
|
@@ -4,468 +4,104 @@
|
|
| 4 |
"cell_type": "markdown",
|
| 5 |
"metadata": {},
|
| 6 |
"source": [
|
| 7 |
-
"# MaterialForge: GRPO Training
|
| 8 |
"\n",
|
| 9 |
-
"**Meta PyTorch OpenEnv Hackathon
|
| 10 |
"\n",
|
| 11 |
-
"This notebook trains
|
| 12 |
-
"\n",
|
| 13 |
-
"| Component | Details |\n",
|
| 14 |
-
"|-----------|----------|\n",
|
| 15 |
-
"| **Environment** | MaterialForge \u2014 8x8 atomic lattice, 4 atom species, 4 physical properties |\n",
|
| 16 |
-
"| **Algorithm** | GRPO (Group Relative Policy Optimization) via HuggingFace TRL |\n",
|
| 17 |
-
"| **Efficiency** | Unsloth for 4-bit QLoRA training |\n",
|
| 18 |
-
"| **Framework** | OpenEnv-compliant environment |\n",
|
| 19 |
-
"| **HF Space** | [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env) |"
|
| 20 |
-
]
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"cell_type": "markdown",
|
| 24 |
-
"metadata": {},
|
| 25 |
-
"source": [
|
| 26 |
-
"## 1. Installation"
|
| 27 |
-
]
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"cell_type": "code",
|
| 31 |
-
"metadata": {
|
| 32 |
-
"jupyter": {
|
| 33 |
-
"is_executing": true
|
| 34 |
-
}
|
| 35 |
-
},
|
| 36 |
-
"source": "%%capture\n!pip install -Uq unsloth\n!pip install -Uq trl>=0.18.0 transformers datasets accelerate peft\n!pip install -Uq \"openenv-core[core]>=0.2.3\" aiofiles openai python-dotenv\n!pip install -Uq matplotlib jmespath",
|
| 37 |
-
"outputs": [],
|
| 38 |
-
"execution_count": null
|
| 39 |
-
},
|
| 40 |
-
{
|
| 41 |
-
"cell_type": "markdown",
|
| 42 |
-
"metadata": {},
|
| 43 |
-
"source": [
|
| 44 |
-
"## 2. Clone Environment & Setup Paths\n",
|
| 45 |
-
"\n",
|
| 46 |
-
"We clone the MaterialForge repository to access the environment code directly.\n",
|
| 47 |
-
"The same environment is hosted live at: https://huggingface.co/spaces/ArshPathan/material_forge_env"
|
| 48 |
-
]
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"cell_type": "code",
|
| 52 |
-
"execution_count": null,
|
| 53 |
-
"metadata": {},
|
| 54 |
-
"outputs": [],
|
| 55 |
-
"source": [
|
| 56 |
-
"import os\n",
|
| 57 |
-
"\n",
|
| 58 |
-
"REPO_URL = \"https://huggingface.co/spaces/ArshPathan/material_forge_env\"\n",
|
| 59 |
-
"REPO_DIR = \"MaterialForge\"\n",
|
| 60 |
-
"\n",
|
| 61 |
-
"if not os.path.isdir(REPO_DIR):\n",
|
| 62 |
-
" !git clone {REPO_URL} {REPO_DIR}\n",
|
| 63 |
-
"else:\n",
|
| 64 |
-
" print(f\"{REPO_DIR}/ already exists, skipping clone.\")\n",
|
| 65 |
-
"\n",
|
| 66 |
-
"import sys\n",
|
| 67 |
-
"sys.path.insert(0, REPO_DIR)\n",
|
| 68 |
-
"\n",
|
| 69 |
-
"print(\"Environment source ready.\")"
|
| 70 |
-
]
|
| 71 |
-
},
|
| 72 |
-
{
|
| 73 |
-
"cell_type": "markdown",
|
| 74 |
-
"metadata": {},
|
| 75 |
-
"source": [
|
| 76 |
-
"## 3. Verify Environment Works\n",
|
| 77 |
-
"\n",
|
| 78 |
-
"Quick sanity check: reset the environment, take one action, confirm reward is computed."
|
| 79 |
-
]
|
| 80 |
-
},
|
| 81 |
-
{
|
| 82 |
-
"cell_type": "code",
|
| 83 |
-
"execution_count": null,
|
| 84 |
-
"metadata": {},
|
| 85 |
-
"outputs": [],
|
| 86 |
-
"source": [
|
| 87 |
-
"from environment.rubrics import HeuristicRewardRubric\n",
|
| 88 |
-
"from server.material_forge_env_environment import MaterialForgeEnvironment\n",
|
| 89 |
-
"from models import MaterialForgeAction\n",
|
| 90 |
-
"\n",
|
| 91 |
-
"env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n",
|
| 92 |
-
"obs = env.reset(seed=42, difficulty=\"easy\")\n",
|
| 93 |
-
"print(f\"Target properties: {obs.target}\")\n",
|
| 94 |
-
"print(f\"Cost budget: {obs.cost_budget}\")\n",
|
| 95 |
-
"print(f\"Max steps: {obs.max_steps}\")\n",
|
| 96 |
-
"\n",
|
| 97 |
-
"action = MaterialForgeAction(action_type=\"place\", row=3, col=3, atom=\"A\")\n",
|
| 98 |
-
"obs = env.step(action)\n",
|
| 99 |
-
"print(f\"\\nAfter placing Metal at (3,3):\")\n",
|
| 100 |
-
"print(f\" Reward: {obs.reward}\")\n",
|
| 101 |
-
"print(f\" Phase: {obs.phase}\")\n",
|
| 102 |
-
"print(f\" Properties: {obs.current_properties}\")\n",
|
| 103 |
-
"print(\"\\nEnvironment OK!\")"
|
| 104 |
-
]
|
| 105 |
-
},
|
| 106 |
-
{
|
| 107 |
-
"cell_type": "markdown",
|
| 108 |
-
"metadata": {},
|
| 109 |
-
"source": [
|
| 110 |
-
"## 4. TRL Environment Wrapper\n",
|
| 111 |
-
"\n",
|
| 112 |
-
"TRL's `GRPOTrainer` with `environment_factory` requires a wrapper class that exposes **named tool methods** (not a generic `step()`). The trainer discovers these methods and presents them to the LLM as callable tools.\n",
|
| 113 |
-
"\n",
|
| 114 |
-
"Our wrapper exposes three tools:\n",
|
| 115 |
-
"- `place_atom(row, col, atom)` \u2014 place a new atom on an empty cell\n",
|
| 116 |
-
"- `remove_atom(row, col)` \u2014 remove an existing atom\n",
|
| 117 |
-
"- `replace_atom(row, col, atom)` \u2014 swap an atom for a different species\n",
|
| 118 |
-
"\n",
|
| 119 |
-
"Each tool internally calls the OpenEnv environment's standard `step()` method."
|
| 120 |
-
]
|
| 121 |
-
},
|
| 122 |
-
{
|
| 123 |
-
"cell_type": "code",
|
| 124 |
-
"execution_count": null,
|
| 125 |
-
"metadata": {},
|
| 126 |
-
"outputs": [],
|
| 127 |
-
"source": "import random\nfrom typing import Optional\n\nfrom environment.config import GRID_SIZE, PROPERTY_NAMES, ATOM_TYPES\nfrom environment.rubrics import HeuristicRewardRubric\nfrom models import MaterialForgeAction, MaterialForgeObservation\nfrom server.material_forge_env_environment import MaterialForgeEnvironment\n\n\nDIFFICULTIES = [\"easy\", \"medium\", \"hard\"]\n\n\nclass MaterialForgeTRLEnv:\n \"\"\"TRL-compatible wrapper for MaterialForge crystal design.\"\"\"\n\n def __init__(self):\n self.env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n self.reward = 0.0\n self.done = False\n self._obs = None\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n\n def reset(self, **kwargs) -> str:\n seed = random.randint(0, 99999)\n difficulty = random.choice(DIFFICULTIES)\n obs = self.env.reset(seed=seed, difficulty=difficulty)\n self._obs = obs\n self.reward = 0.0\n self.done = False\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n return self._format_observation(obs)\n\n def place_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Place an atom on an empty cell of the crystal lattice.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: Atom species - 'A' (Metal, hardness, cost 8), 'B' (Conductor, conductivity, cost 6), 'C' (Ceramic, thermal resistance, cost 4), or 'P' (Polymer, elasticity, cost 2)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"place\", row, col, atom)\n\n def remove_atom(self, row: int, col: int) -> str:\n \"\"\"Remove an atom from the crystal lattice to free up budget.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"remove\", row, col, None)\n\n def replace_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Replace an existing atom with a different species to adjust material properties.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: New atom species - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"replace\", row, col, atom)\n\n def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:\n if self.done:\n raise ValueError(\"Episode is over. The crystal design session has ended.\")\n\n self._total_actions += 1\n row = max(0, min(int(row), GRID_SIZE - 1))\n col = max(0, min(int(col), GRID_SIZE - 1))\n\n if atom is not None:\n atom = str(atom).upper().strip()\n if atom not in ATOM_TYPES:\n atom = \"A\"\n\n # Pre-check: will this action actually change the grid?\n grid = self._obs.grid if self._obs else None\n if grid:\n cell = grid[row][col]\n if action_type == \"place\" and cell != \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot place at ({row},{col}) \u2014 cell is already \"\n f\"occupied by '{cell}'. Use replace_atom to change it, or pick an empty cell (marked '.').\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"remove\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot remove from ({row},{col}) \u2014 cell is empty. \"\n f\"Pick a cell that contains an atom.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot replace at ({row},{col}) \u2014 cell is empty. \"\n f\"Use place_atom instead.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == atom:\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cell ({row},{col}) already contains '{atom}'. \"\n f\"Pick a different atom species to replace it with.\\n\"\n + self._format_observation(self._obs)\n )\n\n action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)\n obs = self.env.step(action)\n self._obs = obs\n step_reward = obs.reward if obs.reward is not None else 0.0\n self._reward_history.append(step_reward)\n self._best_reward = max(self._best_reward, step_reward)\n self.reward = self._best_reward\n self.done = obs.done\n return self._format_observation(obs)\n\n def _format_observation(self, obs) -> str:\n grid_lines = []\n for r, row in enumerate(obs.grid):\n grid_lines.append(f\" {r}: {' '.join(cell if cell != '.' else '.' for cell in row)}\")\n grid_str = \"\\n\".join(grid_lines)\n\n gaps = {}\n for prop in PROPERTY_NAMES:\n t = obs.target.get(prop, 0.0)\n c = obs.current_properties.get(prop, 0.0)\n gaps[prop] = round(t - c, 1)\n\n props_str = \", \".join(\n f\"{p}={obs.current_properties.get(p, 0):.1f}/{obs.target.get(p, 0):.1f}(gap:{gaps[p]:+.1f})\"\n for p in PROPERTY_NAMES\n )\n\n sb = obs.score_breakdown or {}\n stability = sb.get(\"structural_stability\", 0.0)\n order = sb.get(\"lattice_order_index\", 0.0)\n\n return (\n f\"Step {obs.step_number}/{obs.max_steps} | \"\n f\"Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | \"\n f\"Phase: {obs.phase} | \"\n f\"Reward: {obs.reward:.4f}\\n\"\n f\"Properties: {props_str}\\n\"\n f\"Stability: {stability:.3f} | Order: {order:.3f}\\n\"\n f\"Grid (row: cols 0-7):\\n{grid_str}\"\n )\n\n\nprint(\"MaterialForgeTRLEnv defined.\")"
|
| 128 |
-
},
|
| 129 |
-
{
|
| 130 |
-
"cell_type": "markdown",
|
| 131 |
-
"metadata": {},
|
| 132 |
-
"source": [
|
| 133 |
-
"### Test the wrapper"
|
| 134 |
-
]
|
| 135 |
-
},
|
| 136 |
-
{
|
| 137 |
-
"cell_type": "code",
|
| 138 |
-
"execution_count": null,
|
| 139 |
-
"metadata": {},
|
| 140 |
-
"outputs": [],
|
| 141 |
-
"source": [
|
| 142 |
-
"test_env = MaterialForgeTRLEnv()\n",
|
| 143 |
-
"init_obs = test_env.reset()\n",
|
| 144 |
-
"print(\"=== Initial State ===\")\n",
|
| 145 |
-
"print(init_obs)\n",
|
| 146 |
-
"\n",
|
| 147 |
-
"print(\"\\n=== After place_atom(3, 3, 'A') ===\")\n",
|
| 148 |
-
"result = test_env.place_atom(3, 3, \"A\")\n",
|
| 149 |
-
"print(result)\n",
|
| 150 |
-
"print(f\"\\nReward stored: {test_env.reward:.4f}\")\n",
|
| 151 |
-
"print(f\"Done: {test_env.done}\")\n",
|
| 152 |
-
"\n",
|
| 153 |
-
"print(\"\\n=== After place_atom(3, 4, 'B') ===\")\n",
|
| 154 |
-
"result = test_env.place_atom(3, 4, \"B\")\n",
|
| 155 |
-
"print(result)\n",
|
| 156 |
-
"print(f\"Reward: {test_env.reward:.4f}\")"
|
| 157 |
-
]
|
| 158 |
-
},
|
| 159 |
-
{
|
| 160 |
-
"cell_type": "markdown",
|
| 161 |
-
"metadata": {},
|
| 162 |
-
"source": [
|
| 163 |
-
"## 5. Reward Function"
|
| 164 |
-
]
|
| 165 |
-
},
|
| 166 |
-
{
|
| 167 |
-
"cell_type": "code",
|
| 168 |
-
"execution_count": null,
|
| 169 |
-
"metadata": {},
|
| 170 |
-
"outputs": [],
|
| 171 |
-
"source": [
|
| 172 |
-
"def reward_func(environments, **kwargs) -> list[float]:\n \"\"\"Reward = best_reward + spatial_bonus + phase_bonus - invalid_penalty.\"\"\"\n rewards = []\n for env in environments:\n best = env._best_reward\n total = max(env._total_actions, 1)\n\n # Penalty for invalid actions\n invalid_ratio = env._invalid_actions / total\n invalid_penalty = 0.3 * invalid_ratio\n\n # Spatial diversity bonus: reward using multiple rows and columns\n spatial_bonus = 0.0\n if env._obs is not None:\n grid = env._obs.grid\n rows_used = set()\n cols_used = set()\n for r, row in enumerate(grid):\n for c, cell in enumerate(row):\n if cell != \".\":\n rows_used.add(r)\n cols_used.add(c)\n # Bonus for spreading across rows AND columns (max 0.10)\n row_spread = min(len(rows_used) / 4.0, 1.0) # 4+ rows = full bonus\n col_spread = min(len(cols_used) / 4.0, 1.0) # 4+ cols = full bonus\n spatial_bonus = 0.10 * (row_spread * col_spread)\n\n # Phase bonus\n phase_bonus = 0.0\n if env._obs is not None:\n phase = env._obs.phase\n if phase == \"crystalline\":\n phase_bonus = 0.15\n elif phase == \"polycrystalline\":\n phase_bonus = 0.05\n\n final = best + spatial_bonus + phase_bonus - invalid_penalty\n rewards.append(max(min(final, 1.0), 0.0))\n return rewards\n\nprint(\"Reward function: best_reward + spatial_bonus + phase_bonus - invalid_penalty\")"
|
| 173 |
-
]
|
| 174 |
-
},
|
| 175 |
-
{
|
| 176 |
-
"cell_type": "markdown",
|
| 177 |
-
"metadata": {},
|
| 178 |
-
"source": [
|
| 179 |
-
"## 6. Training Dataset\n",
|
| 180 |
-
"\n",
|
| 181 |
-
"We create a prompt dataset where each entry instructs the LLM to design a crystal.\n",
|
| 182 |
-
"The environment generates random targets on each `reset()`, so every episode is unique."
|
| 183 |
-
]
|
| 184 |
-
},
|
| 185 |
-
{
|
| 186 |
-
"cell_type": "code",
|
| 187 |
-
"execution_count": null,
|
| 188 |
-
"metadata": {},
|
| 189 |
-
"outputs": [],
|
| 190 |
-
"source": [
|
| 191 |
-
"from datasets import Dataset\n\nSYSTEM_PROMPT = \"\"\"You are an expert materials scientist designing crystal structures on an 8x8 atomic lattice.\n\nYour goal: place atoms to match target physical properties while staying within budget.\n\nAvailable atom species:\n- A (Metal): Primary contribution to hardness, cost 8 per atom\n- B (Conductor): Primary contribution to conductivity, cost 6 per atom\n- C (Ceramic): Primary contribution to thermal resistance, cost 4 per atom\n- P (Polymer): Primary contribution to elasticity, cost 2 per atom\n\nStrategy:\n- START from the center of the grid (around row 3-4, col 3-4) and expand outward in all directions\n- Build 2D clusters, NOT single rows or columns \u2014 atoms need neighbors in multiple directions for bonding\n- Group same-type atoms together in 2x2 or 3x3 blocks for maximum bonding bonuses\n- After each placement, READ the property gaps and choose the atom type that closes the LARGEST gap\n- Aim for crystalline phase: fill a compact rectangular region, not scattered atoms\n- Stay within the cost budget \u2014 use cheaper atoms (P=2, C=4) when budget is tight\n- Use replace_atom to fix overshooting properties, remove_atom if over budget\n\nUse the tools place_atom, remove_atom, and replace_atom to build your crystal.\"\"\"\n\nUSER_PROMPT = \"Design a crystal lattice that matches the target material properties. Start placing atoms from the center of the grid (row 3-4, col 3-4) and expand outward in all directions. Analyze the property gaps after each action and choose your next atom strategically.\"\n\nNUM_EPISODES = 200\n\ndataset = Dataset.from_dict({\n \"prompt\": [\n [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": USER_PROMPT},\n ]\n ] * NUM_EPISODES\n})\n\nprint(f\"Dataset created: {len(dataset)} episodes\")\nprint(f\"Sample prompt roles: {[m['role'] for m in dataset[0]['prompt']]}\")"
|
| 192 |
-
]
|
| 193 |
-
},
|
| 194 |
-
{
|
| 195 |
-
"cell_type": "markdown",
|
| 196 |
-
"metadata": {},
|
| 197 |
-
"source": [
|
| 198 |
-
"## 7. Load Model with Unsloth\n",
|
| 199 |
-
"\n",
|
| 200 |
-
"We use Unsloth for memory-efficient 4-bit QLoRA fine-tuning. Starting with `Qwen/Qwen3-0.6B` which fits on a free Colab T4 GPU. For better results with more compute, switch to `Qwen/Qwen3-1.7B` or `Qwen/Qwen2.5-3B-Instruct`."
|
| 201 |
-
]
|
| 202 |
-
},
|
| 203 |
-
{
|
| 204 |
-
"cell_type": "code",
|
| 205 |
-
"execution_count": null,
|
| 206 |
-
"metadata": {},
|
| 207 |
-
"outputs": [],
|
| 208 |
-
"source": "import torch\nfrom unsloth import FastLanguageModel\n\nMODEL_NAME = \"Qwen/Qwen3-0.6B\" # Change to Qwen3-1.7B or Qwen2.5-3B-Instruct for better results\nMAX_SEQ_LENGTH = 4096\n\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n MODEL_NAME,\n max_seq_length=MAX_SEQ_LENGTH,\n load_in_4bit=True,\n dtype=None,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n model,\n r=16,\n lora_alpha=16,\n lora_dropout=0,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n \"gate_proj\", \"up_proj\", \"down_proj\"],\n use_gradient_checkpointing=\"unsloth\",\n)\n\n# --- Fix Unsloth + TRL compatibility for Qwen3 tool calling ---\n# Unsloth modifies the chat template, breaking TRL's string-comparison checks.\n# We load TRL's own Qwen3 training template (prefix-preserving, has {% generation %}\n# markers) and response schema directly.\nfrom pathlib import Path\nimport trl.chat_template_utils as _ctu\n\n# 1) Set the response schema so TRL can parse <tool_call> blocks\ntokenizer.response_schema = {\n \"x-regex\": r\"^(?:<think>\\n?(?:(?P<reasoning_content>.*?\\S.*?)\\n?|[\\s]*)</think>\\s*)?(?P<content>.*?)(?:\\n(?=<tool_call>))?(?=(?:<tool_call>|<\\|im_end\\|>|$))(?P<tool_calls>(?:<tool_call>.+?</tool_call>\\s*)+)?\\s*(?:<\\|im_end\\|>|$)\",\n \"type\": \"object\",\n \"properties\": {\n \"role\": {\"const\": \"assistant\"},\n \"content\": {\"type\": \"string\"},\n \"reasoning_content\": {\"type\": \"string\"},\n \"tool_calls\": {\n \"type\": \"array\",\n \"x-regex-iterator\": r\"<tool_call>\\s*(.+?)\\s*</tool_call>\",\n \"items\": {\n \"x-parser\": \"json\",\n \"x-parser-args\": {\"transform\": \"{type: 'function', function: @}\"},\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\"const\": \"function\"},\n \"function\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\"type\": \"string\"},\n \"arguments\": {\"type\": \"object\", \"additionalProperties\": {}},\n },\n },\n },\n },\n },\n },\n}\n\n# 2) Replace Unsloth's modified template with TRL's training-safe Qwen3 template\n_templates_dir = Path(_ctu.__file__).parent / \"chat_templates\"\ntokenizer.chat_template = (_templates_dir / \"qwen3_training.jinja\").read_text()\n\nprint(f\"Model loaded: {MODEL_NAME}\")\nprint(f\"Trainable params: {model.print_trainable_parameters()}\")\nprint(f\"Chat template: TRL qwen3_training.jinja (prefix-preserving)\")\nprint(f\"Response schema: Qwen3 tool-call parser\")"
|
| 209 |
-
},
|
| 210 |
-
{
|
| 211 |
-
"cell_type": "markdown",
|
| 212 |
-
"metadata": {},
|
| 213 |
-
"source": [
|
| 214 |
-
"## 8. GRPO Training\n",
|
| 215 |
-
"\n",
|
| 216 |
-
"The `GRPOTrainer` with `environment_factory` handles the full multi-turn loop:\n",
|
| 217 |
-
"1. Generates completions from the LLM\n",
|
| 218 |
-
"2. Parses tool calls (`place_atom`, `remove_atom`, `replace_atom`)\n",
|
| 219 |
-
"3. Executes them against the MaterialForge environment\n",
|
| 220 |
-
"4. Feeds observations back to the model\n",
|
| 221 |
-
"5. Collects rewards and updates the policy via GRPO"
|
| 222 |
-
]
|
| 223 |
-
},
|
| 224 |
-
{
|
| 225 |
-
"cell_type": "code",
|
| 226 |
-
"execution_count": null,
|
| 227 |
-
"metadata": {},
|
| 228 |
-
"outputs": [],
|
| 229 |
-
"source": [
|
| 230 |
-
"from trl import GRPOConfig, GRPOTrainer\n\nOUTPUT_DIR = \"materialforge-grpo-checkpoints\"\n\ntraining_args = GRPOConfig(\n output_dir=OUTPUT_DIR,\n\n # Generation\n num_generations=4,\n max_completion_length=2048,\n temperature=1.0,\n\n # Optimization\n learning_rate=5e-5,\n per_device_train_batch_size=1,\n gradient_accumulation_steps=4,\n num_train_epochs=1,\n\n # Logging\n logging_steps=1,\n log_completions=True,\n num_completions_to_print=1,\n\n # Tool calling\n chat_template_kwargs={\"enable_thinking\": False},\n\n # Saving\n save_steps=50,\n save_total_limit=2,\n)\n\ntrainer = GRPOTrainer(\n model=model,\n processing_class=tokenizer,\n reward_funcs=reward_func,\n train_dataset=dataset,\n args=training_args,\n environment_factory=MaterialForgeTRLEnv,\n)\n\nprint(\"Trainer initialized. Starting GRPO training...\")\nprint(f\" Episodes: {NUM_EPISODES}\")\nprint(f\" Generations per prompt: {training_args.num_generations}\")\nprint(f\" Max completion length: {training_args.max_completion_length}\")\nprint(f\" Temperature: {training_args.temperature}\")\nprint(f\" Learning rate: {training_args.learning_rate}\")\nprint(f\" Gradient accumulation: {training_args.gradient_accumulation_steps}\")\nprint(f\" Total steps: {NUM_EPISODES // (training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps)}\")"
|
| 231 |
]
|
| 232 |
},
|
| 233 |
{
|
| 234 |
"cell_type": "code",
|
| 235 |
-
"execution_count": null,
|
| 236 |
"metadata": {},
|
| 237 |
-
"outputs": [],
|
| 238 |
"source": [
|
| 239 |
-
"
|
| 240 |
-
"
|
| 241 |
-
|
| 242 |
-
"print(f\" Final loss: {train_result.training_loss:.4f}\")"
|
| 243 |
-
]
|
| 244 |
-
},
|
| 245 |
-
{
|
| 246 |
-
"cell_type": "markdown",
|
| 247 |
-
"metadata": {},
|
| 248 |
-
"source": [
|
| 249 |
-
"## 9. Save Trained Model"
|
| 250 |
-
]
|
| 251 |
-
},
|
| 252 |
-
{
|
| 253 |
-
"cell_type": "code",
|
| 254 |
-
"execution_count": null,
|
| 255 |
-
"metadata": {},
|
| 256 |
"outputs": [],
|
| 257 |
-
"
|
| 258 |
-
"SAVE_DIR = \"materialforge-grpo-trained\"\n",
|
| 259 |
-
"trainer.save_model(SAVE_DIR)\n",
|
| 260 |
-
"tokenizer.save_pretrained(SAVE_DIR)\n",
|
| 261 |
-
"print(f\"Model saved to {SAVE_DIR}/\")"
|
| 262 |
-
]
|
| 263 |
-
},
|
| 264 |
-
{
|
| 265 |
-
"cell_type": "markdown",
|
| 266 |
-
"metadata": {},
|
| 267 |
-
"source": [
|
| 268 |
-
"## 10. Training Reward Curves\n",
|
| 269 |
-
"\n",
|
| 270 |
-
"Extract and plot the reward history from training logs. These plots are a **required deliverable** \u2014 they must be saved as images and embedded in the README."
|
| 271 |
-
]
|
| 272 |
-
},
|
| 273 |
-
{
|
| 274 |
-
"cell_type": "code",
|
| 275 |
-
"execution_count": null,
|
| 276 |
-
"metadata": {},
|
| 277 |
-
"outputs": [],
|
| 278 |
-
"source": [
|
| 279 |
-
"import matplotlib.pyplot as plt\n",
|
| 280 |
-
"import json\n",
|
| 281 |
-
"\n",
|
| 282 |
-
"os.makedirs(\"plots\", exist_ok=True)\n",
|
| 283 |
-
"\n",
|
| 284 |
-
"log_history = trainer.state.log_history\n",
|
| 285 |
-
"\n",
|
| 286 |
-
"steps = []\n",
|
| 287 |
-
"rewards = []\n",
|
| 288 |
-
"losses = []\n",
|
| 289 |
-
"\n",
|
| 290 |
-
"for entry in log_history:\n",
|
| 291 |
-
" if \"reward\" in entry:\n",
|
| 292 |
-
" steps.append(entry.get(\"step\", len(steps)))\n",
|
| 293 |
-
" rewards.append(entry[\"reward\"])\n",
|
| 294 |
-
" if \"loss\" in entry:\n",
|
| 295 |
-
" if len(losses) < len(steps):\n",
|
| 296 |
-
" losses.append(entry[\"loss\"])\n",
|
| 297 |
-
"\n",
|
| 298 |
-
"# --- Plot 1: Reward over training ---\n",
|
| 299 |
-
"fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 300 |
-
"ax.plot(steps[:len(rewards)], rewards, color=\"#4285f4\", linewidth=2, label=\"GRPO Reward\")\n",
|
| 301 |
-
"if len(rewards) > 5:\n",
|
| 302 |
-
" window = max(3, len(rewards) // 10)\n",
|
| 303 |
-
" smoothed = [sum(rewards[max(0,i-window):i+1])/len(rewards[max(0,i-window):i+1]) for i in range(len(rewards))]\n",
|
| 304 |
-
" ax.plot(steps[:len(smoothed)], smoothed, color=\"#ea4335\", linewidth=2, linestyle=\"--\", label=f\"Smoothed (window={window})\")\n",
|
| 305 |
-
"ax.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 306 |
-
"ax.set_ylabel(\"Reward\", fontsize=12)\n",
|
| 307 |
-
"ax.set_title(\"MaterialForge GRPO Training \u2014 Reward Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 308 |
-
"ax.legend()\n",
|
| 309 |
-
"ax.grid(True, alpha=0.3)\n",
|
| 310 |
-
"fig.tight_layout()\n",
|
| 311 |
-
"fig.savefig(\"plots/reward_curve.png\", dpi=150)\n",
|
| 312 |
-
"plt.show()\n",
|
| 313 |
-
"print(\"Saved: plots/reward_curve.png\")\n",
|
| 314 |
-
"\n",
|
| 315 |
-
"# --- Plot 2: Loss over training ---\n",
|
| 316 |
-
"if losses:\n",
|
| 317 |
-
" fig2, ax2 = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 318 |
-
" ax2.plot(range(len(losses)), losses, color=\"#34a853\", linewidth=2)\n",
|
| 319 |
-
" ax2.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 320 |
-
" ax2.set_ylabel(\"Loss\", fontsize=12)\n",
|
| 321 |
-
" ax2.set_title(\"MaterialForge GRPO Training \u2014 Loss Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 322 |
-
" ax2.grid(True, alpha=0.3)\n",
|
| 323 |
-
" fig2.tight_layout()\n",
|
| 324 |
-
" fig2.savefig(\"plots/loss_curve.png\", dpi=150)\n",
|
| 325 |
-
" plt.show()\n",
|
| 326 |
-
" print(\"Saved: plots/loss_curve.png\")"
|
| 327 |
-
]
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
"cell_type": "markdown",
|
| 331 |
-
"metadata": {},
|
| 332 |
-
"source": [
|
| 333 |
-
"## 11. Baseline Comparison\n",
|
| 334 |
-
"\n",
|
| 335 |
-
"Compare the trained agent against a random baseline to demonstrate improvement."
|
| 336 |
-
]
|
| 337 |
},
|
| 338 |
{
|
| 339 |
"cell_type": "code",
|
| 340 |
-
"execution_count": null,
|
| 341 |
"metadata": {},
|
| 342 |
-
"outputs": [],
|
| 343 |
"source": [
|
| 344 |
-
"
|
| 345 |
-
"\n",
|
| 346 |
-
"
|
| 347 |
-
"
|
| 348 |
-
"
|
| 349 |
-
"
|
| 350 |
-
"
|
| 351 |
-
"\n",
|
| 352 |
-
"
|
| 353 |
-
"
|
| 354 |
-
"
|
| 355 |
-
"
|
| 356 |
-
"
|
| 357 |
-
"
|
| 358 |
-
"
|
| 359 |
-
"
|
| 360 |
-
"
|
| 361 |
-
"
|
| 362 |
-
"
|
| 363 |
-
"
|
| 364 |
-
"
|
| 365 |
-
"
|
| 366 |
-
"
|
| 367 |
-
"\n",
|
| 368 |
-
"
|
| 369 |
-
"\n",
|
| 370 |
-
"
|
| 371 |
-
"
|
| 372 |
-
"
|
| 373 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
]
|
| 375 |
},
|
| 376 |
{
|
| 377 |
"cell_type": "code",
|
| 378 |
-
"execution_count": null,
|
| 379 |
"metadata": {},
|
| 380 |
-
"outputs": [],
|
| 381 |
"source": [
|
| 382 |
-
"
|
| 383 |
-
"
|
| 384 |
-
"random_mean_reward = sum(random_avg_rewards) / len(random_avg_rewards)\n",
|
| 385 |
"\n",
|
| 386 |
-
"
|
| 387 |
-
"
|
| 388 |
-
"means = [random_mean_reward, trained_mean_reward]\n",
|
| 389 |
-
"colors = [\"#ea4335\", \"#4285f4\"]\n",
|
| 390 |
-
"bars = ax3.bar(agents, means, color=colors, width=0.5, edgecolor=\"white\", linewidth=1.5)\n",
|
| 391 |
-
"\n",
|
| 392 |
-
"for bar, val in zip(bars, means):\n",
|
| 393 |
-
" ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n",
|
| 394 |
-
" f\"{val:.4f}\", ha=\"center\", va=\"bottom\", fontweight=\"bold\", fontsize=13)\n",
|
| 395 |
-
"\n",
|
| 396 |
-
"ax3.set_ylabel(\"Mean Reward\", fontsize=12)\n",
|
| 397 |
-
"ax3.set_title(\"MaterialForge \u2014 Random Baseline vs GRPO-Trained Agent\", fontsize=14, fontweight=\"bold\")\n",
|
| 398 |
-
"ax3.set_ylim(0, max(means) * 1.3 + 0.05)\n",
|
| 399 |
-
"ax3.grid(axis=\"y\", alpha=0.3)\n",
|
| 400 |
-
"fig3.tight_layout()\n",
|
| 401 |
-
"fig3.savefig(\"plots/baseline_comparison.png\", dpi=150)\n",
|
| 402 |
-
"plt.show()\n",
|
| 403 |
-
"print(\"Saved: plots/baseline_comparison.png\")"
|
| 404 |
-
]
|
| 405 |
-
},
|
| 406 |
-
{
|
| 407 |
-
"cell_type": "markdown",
|
| 408 |
-
"metadata": {},
|
| 409 |
-
"source": [
|
| 410 |
-
"## 12. Trained Agent Inference Demo\n",
|
| 411 |
"\n",
|
| 412 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
]
|
| 414 |
},
|
| 415 |
{
|
| 416 |
"cell_type": "code",
|
| 417 |
-
"execution_count": null,
|
| 418 |
"metadata": {},
|
| 419 |
-
"outputs": [],
|
| 420 |
"source": [
|
| 421 |
-
"from
|
| 422 |
"\n",
|
| 423 |
-
"
|
| 424 |
-
"
|
| 425 |
-
"
|
| 426 |
-
"
|
| 427 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
")\n",
|
| 429 |
-
"FastLanguageModel.for_inference(inf_model)\n",
|
| 430 |
"\n",
|
| 431 |
-
"
|
| 432 |
-
"
|
| 433 |
-
"\n",
|
| 434 |
-
"
|
| 435 |
-
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 436 |
-
" {\"role\": \"user\", \"content\": USER_PROMPT},\n",
|
| 437 |
-
" {\"role\": \"assistant\", \"content\": f\"Let me analyze the initial state:\\n{obs_text}\\n\\nI'll start placing atoms strategically.\"},\n",
|
| 438 |
-
"]\n",
|
| 439 |
-
"\n",
|
| 440 |
-
"print(\"=\" * 60)\n",
|
| 441 |
-
"print(\"TRAINED AGENT DEMO\")\n",
|
| 442 |
-
"print(\"=\" * 60)\n",
|
| 443 |
-
"print(f\"\\nInitial observation:\\n{obs_text}\")\n",
|
| 444 |
-
"print(f\"\\nAgent designing crystal...\")\n",
|
| 445 |
-
"print(f\"(Full multi-turn inference requires TRL's tool-call loop.\")\n",
|
| 446 |
-
"print(f\"See the training logs above for actual agent behavior.)\")"
|
| 447 |
-
]
|
| 448 |
-
},
|
| 449 |
-
{
|
| 450 |
-
"cell_type": "markdown",
|
| 451 |
-
"metadata": {},
|
| 452 |
-
"source": [
|
| 453 |
-
"## 13. Summary\n",
|
| 454 |
-
"\n",
|
| 455 |
-
"| Metric | Random Baseline | GRPO-Trained Agent |\n",
|
| 456 |
-
"|--------|----------------|--------------------|\n",
|
| 457 |
-
"| Mean Reward | *see above* | *see above* |\n",
|
| 458 |
-
"| Phase Achieved | Amorphous | *see training logs* |\n",
|
| 459 |
-
"\n",
|
| 460 |
-
"### Deliverables produced by this notebook:\n",
|
| 461 |
-
"- `plots/reward_curve.png` \u2014 Reward over training steps\n",
|
| 462 |
-
"- `plots/loss_curve.png` \u2014 Training loss curve\n",
|
| 463 |
-
"- `plots/baseline_comparison.png` \u2014 Random vs trained comparison\n",
|
| 464 |
-
"- `materialforge-grpo-trained/` \u2014 Saved model checkpoint\n",
|
| 465 |
"\n",
|
| 466 |
-
"
|
| 467 |
-
"- **Live Environment**: [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env)\n",
|
| 468 |
-
"- **Repository**: [MaterialForge GitHub](https://github.com/Arsh-Pathan/MaterialForge)"
|
| 469 |
]
|
| 470 |
}
|
| 471 |
],
|
|
@@ -473,8 +109,7 @@
|
|
| 473 |
"accelerator": "GPU",
|
| 474 |
"colab": {
|
| 475 |
"gpuType": "T4",
|
| 476 |
-
"
|
| 477 |
-
"name": "MaterialForge_GRPO_Training.ipynb"
|
| 478 |
},
|
| 479 |
"kernelspec": {
|
| 480 |
"display_name": "Python 3",
|
|
@@ -487,4 +122,4 @@
|
|
| 487 |
},
|
| 488 |
"nbformat": 4,
|
| 489 |
"nbformat_minor": 0
|
| 490 |
-
}
|
|
|
|
| 4 |
"cell_type": "markdown",
|
| 5 |
"metadata": {},
|
| 6 |
"source": [
|
| 7 |
+
"# MaterialForge: Reasoning-Based GRPO Training (100 Episode Sprint)\n",
|
| 8 |
"\n",
|
| 9 |
+
"**Meta PyTorch OpenEnv Hackathon \u2014 Grand Finale**\n",
|
| 10 |
"\n",
|
| 11 |
+
"This notebook trains a **DeepSeek-R1 Reasoning Model** to design specialized crystalline lattices using specialist atomic species."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
]
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"cell_type": "code",
|
|
|
|
| 16 |
"metadata": {},
|
|
|
|
| 17 |
"source": [
|
| 18 |
+
"%%capture\n",
|
| 19 |
+
"!pip install -Uq unsloth trl>=0.18.0 transformers datasets accelerate peft openenv-core[core]>=0.2.3 matplotlib jmespath"
|
| 20 |
+
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
"outputs": [],
|
| 22 |
+
"execution_count": null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
},
|
| 24 |
{
|
| 25 |
"cell_type": "code",
|
|
|
|
| 26 |
"metadata": {},
|
|
|
|
| 27 |
"source": [
|
| 28 |
+
"from training.train_env_wrapper import MaterialForgeTRLEnv\n",
|
| 29 |
+
"from datasets import Dataset\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"def reward_func(environments, completions, **kwargs) -> list[float]:\n",
|
| 32 |
+
" rewards = []\n",
|
| 33 |
+
" for env, completion in zip(environments, completions):\n",
|
| 34 |
+
" # 1. Base Scientific Reward\n",
|
| 35 |
+
" score = env.reward \n",
|
| 36 |
+
" \n",
|
| 37 |
+
" # 2. Reasoning Bonus (Innovation)\n",
|
| 38 |
+
" thought_bonus = 0.0\n",
|
| 39 |
+
" if \"<think>\" in completion and \"</think>\" in completion:\n",
|
| 40 |
+
" thought = completion.split(\"</think>\")[0].split(\"<think>\")[1]\n",
|
| 41 |
+
" if len(thought.strip()) > 100:\n",
|
| 42 |
+
" thought_bonus = 0.05\n",
|
| 43 |
+
"\n",
|
| 44 |
+
" rewards.append(max(min(score + thought_bonus, 1.0), 0.0))\n",
|
| 45 |
+
" return rewards\n",
|
| 46 |
+
"\n",
|
| 47 |
+
"SYSTEM_PROMPT = \"\"\"You are a Material Scientist. You MUST reason inside <think> tags before every action.\n",
|
| 48 |
+
"\n",
|
| 49 |
+
"Strategic Rules:\n",
|
| 50 |
+
"1. Atoms are SPECIALISTS. Metal (A) is for Hardness, Conductor (B) for Conductivity, etc. \n",
|
| 51 |
+
"2. You MUST combine different species to hit multi-property targets.\n",
|
| 52 |
+
"3. Build compact 2D clusters starting from the center (3,3) to maximize stability.\n",
|
| 53 |
+
"4. Pure crystalline blocks are highly stable; avoid 1D lines.\"\"\"\n",
|
| 54 |
+
"\n",
|
| 55 |
+
"# Requesting 100 episodes for a fast finale sprint\n",
|
| 56 |
+
"NUM_EPISODES = 100\n",
|
| 57 |
+
"dataset = Dataset.from_dict({\"prompt\": [[\n",
|
| 58 |
+
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 59 |
+
" {\"role\": \"user\", \"content\": \"Design a crystal lattice. Think step-by-step and mix specialist atoms.\"}\n",
|
| 60 |
+
"]] * NUM_EPISODES})\n",
|
| 61 |
+
"print(f\"Dataset ready for {NUM_EPISODES} reasoning episodes.\")"
|
| 62 |
]
|
| 63 |
},
|
| 64 |
{
|
| 65 |
"cell_type": "code",
|
|
|
|
| 66 |
"metadata": {},
|
|
|
|
| 67 |
"source": [
|
| 68 |
+
"import torch\n",
|
| 69 |
+
"from unsloth import FastLanguageModel\n",
|
|
|
|
| 70 |
"\n",
|
| 71 |
+
"MODEL_NAME = \"unsloth/DeepSeek-R1-Distill-Qwen-1.5B\"\n",
|
| 72 |
+
"model, tokenizer = FastLanguageModel.from_pretrained(MODEL_NAME, max_seq_length=2048, load_in_4bit=True)\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
"\n",
|
| 74 |
+
"model = FastLanguageModel.get_peft_model(\n",
|
| 75 |
+
" model, r=16, lora_alpha=16, lora_dropout=0,\n",
|
| 76 |
+
" target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
|
| 77 |
+
" use_gradient_checkpointing=\"unsloth\"\n",
|
| 78 |
+
")"
|
| 79 |
]
|
| 80 |
},
|
| 81 |
{
|
| 82 |
"cell_type": "code",
|
|
|
|
| 83 |
"metadata": {},
|
|
|
|
| 84 |
"source": [
|
| 85 |
+
"from trl import GRPOConfig, GRPOTrainer\n",
|
| 86 |
"\n",
|
| 87 |
+
"training_args = GRPOConfig(\n",
|
| 88 |
+
" output_dir=\"materialforge-r1-checkpoints\",\n",
|
| 89 |
+
" num_generations=2,\n",
|
| 90 |
+
" max_completion_length=1024,\n",
|
| 91 |
+
" learning_rate=5e-5,\n",
|
| 92 |
+
" per_device_train_batch_size=1,\n",
|
| 93 |
+
" gradient_accumulation_steps=4, # Reduced for faster updates in a 100-episode run\n",
|
| 94 |
+
" num_train_epochs=1,\n",
|
| 95 |
+
" chat_template_kwargs={\"enable_thinking\": True},\n",
|
| 96 |
+
" logging_steps=1,\n",
|
| 97 |
")\n",
|
|
|
|
| 98 |
"\n",
|
| 99 |
+
"trainer = GRPOTrainer(\n",
|
| 100 |
+
" model=model, processing_class=tokenizer, reward_funcs=reward_func,\n",
|
| 101 |
+
" train_dataset=dataset, args=training_args, environment_factory=MaterialForgeTRLEnv\n",
|
| 102 |
+
")\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
"\n",
|
| 104 |
+
"trainer.train()"
|
|
|
|
|
|
|
| 105 |
]
|
| 106 |
}
|
| 107 |
],
|
|
|
|
| 109 |
"accelerator": "GPU",
|
| 110 |
"colab": {
|
| 111 |
"gpuType": "T4",
|
| 112 |
+
"name": "MaterialForge_Reasoning_Training.ipynb"
|
|
|
|
| 113 |
},
|
| 114 |
"kernelspec": {
|
| 115 |
"display_name": "Python 3",
|
|
|
|
| 122 |
},
|
| 123 |
"nbformat": 4,
|
| 124 |
"nbformat_minor": 0
|
| 125 |
+
}
|
training/Traning Runs/Run - I/MaterialForge_GRPO_Training.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
training/Traning Runs/Run - I/baseline_comparison.png
ADDED
|
training/Traning Runs/Run - I/loss_curve.png
ADDED
|
training/Traning Runs/Run - I/reward_curve.png
ADDED
|
training/Traning Runs/Run - II/MaterialForge_GRPO_Training.ipynb
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# MaterialForge: GRPO Training for Crystal Design\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"**Meta PyTorch OpenEnv Hackathon x Scaler School of Technology \u2014 Grand Finale**\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"This notebook trains an LLM to autonomously design crystalline structures using reinforcement learning (GRPO) with the MaterialForge environment.\n",
|
| 12 |
+
"\n",
|
| 13 |
+
"| Component | Details |\n",
|
| 14 |
+
"|-----------|----------|\n",
|
| 15 |
+
"| **Environment** | MaterialForge \u2014 8x8 atomic lattice, 4 atom species, 4 physical properties |\n",
|
| 16 |
+
"| **Algorithm** | GRPO (Group Relative Policy Optimization) via HuggingFace TRL |\n",
|
| 17 |
+
"| **Efficiency** | Unsloth for 4-bit QLoRA training |\n",
|
| 18 |
+
"| **Framework** | OpenEnv-compliant environment |\n",
|
| 19 |
+
"| **HF Space** | [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env) |"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cell_type": "markdown",
|
| 24 |
+
"metadata": {},
|
| 25 |
+
"source": [
|
| 26 |
+
"## 1. Installation"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"cell_type": "code",
|
| 31 |
+
"metadata": {
|
| 32 |
+
"jupyter": {
|
| 33 |
+
"is_executing": true
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"source": "%%capture\n!pip install -Uq unsloth\n!pip install -Uq trl>=0.18.0 transformers datasets accelerate peft\n!pip install -Uq \"openenv-core[core]>=0.2.3\" aiofiles openai python-dotenv\n!pip install -Uq matplotlib jmespath",
|
| 37 |
+
"outputs": [],
|
| 38 |
+
"execution_count": null
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "markdown",
|
| 42 |
+
"metadata": {},
|
| 43 |
+
"source": [
|
| 44 |
+
"## 2. Clone Environment & Setup Paths\n",
|
| 45 |
+
"\n",
|
| 46 |
+
"We clone the MaterialForge repository to access the environment code directly.\n",
|
| 47 |
+
"The same environment is hosted live at: https://huggingface.co/spaces/ArshPathan/material_forge_env"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"cell_type": "code",
|
| 52 |
+
"execution_count": null,
|
| 53 |
+
"metadata": {},
|
| 54 |
+
"outputs": [],
|
| 55 |
+
"source": [
|
| 56 |
+
"import os\n",
|
| 57 |
+
"\n",
|
| 58 |
+
"REPO_URL = \"https://huggingface.co/spaces/ArshPathan/material_forge_env\"\n",
|
| 59 |
+
"REPO_DIR = \"MaterialForge\"\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"if not os.path.isdir(REPO_DIR):\n",
|
| 62 |
+
" !git clone {REPO_URL} {REPO_DIR}\n",
|
| 63 |
+
"else:\n",
|
| 64 |
+
" print(f\"{REPO_DIR}/ already exists, skipping clone.\")\n",
|
| 65 |
+
"\n",
|
| 66 |
+
"import sys\n",
|
| 67 |
+
"sys.path.insert(0, REPO_DIR)\n",
|
| 68 |
+
"\n",
|
| 69 |
+
"print(\"Environment source ready.\")"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"cell_type": "markdown",
|
| 74 |
+
"metadata": {},
|
| 75 |
+
"source": [
|
| 76 |
+
"## 3. Verify Environment Works\n",
|
| 77 |
+
"\n",
|
| 78 |
+
"Quick sanity check: reset the environment, take one action, confirm reward is computed."
|
| 79 |
+
]
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"cell_type": "code",
|
| 83 |
+
"execution_count": null,
|
| 84 |
+
"metadata": {},
|
| 85 |
+
"outputs": [],
|
| 86 |
+
"source": [
|
| 87 |
+
"from environment.rubrics import HeuristicRewardRubric\n",
|
| 88 |
+
"from server.material_forge_env_environment import MaterialForgeEnvironment\n",
|
| 89 |
+
"from models import MaterialForgeAction\n",
|
| 90 |
+
"\n",
|
| 91 |
+
"env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n",
|
| 92 |
+
"obs = env.reset(seed=42, difficulty=\"easy\")\n",
|
| 93 |
+
"print(f\"Target properties: {obs.target}\")\n",
|
| 94 |
+
"print(f\"Cost budget: {obs.cost_budget}\")\n",
|
| 95 |
+
"print(f\"Max steps: {obs.max_steps}\")\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"action = MaterialForgeAction(action_type=\"place\", row=3, col=3, atom=\"A\")\n",
|
| 98 |
+
"obs = env.step(action)\n",
|
| 99 |
+
"print(f\"\\nAfter placing Metal at (3,3):\")\n",
|
| 100 |
+
"print(f\" Reward: {obs.reward}\")\n",
|
| 101 |
+
"print(f\" Phase: {obs.phase}\")\n",
|
| 102 |
+
"print(f\" Properties: {obs.current_properties}\")\n",
|
| 103 |
+
"print(\"\\nEnvironment OK!\")"
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"cell_type": "markdown",
|
| 108 |
+
"metadata": {},
|
| 109 |
+
"source": [
|
| 110 |
+
"## 4. TRL Environment Wrapper\n",
|
| 111 |
+
"\n",
|
| 112 |
+
"TRL's `GRPOTrainer` with `environment_factory` requires a wrapper class that exposes **named tool methods** (not a generic `step()`). The trainer discovers these methods and presents them to the LLM as callable tools.\n",
|
| 113 |
+
"\n",
|
| 114 |
+
"Our wrapper exposes three tools:\n",
|
| 115 |
+
"- `place_atom(row, col, atom)` \u2014 place a new atom on an empty cell\n",
|
| 116 |
+
"- `remove_atom(row, col)` \u2014 remove an existing atom\n",
|
| 117 |
+
"- `replace_atom(row, col, atom)` \u2014 swap an atom for a different species\n",
|
| 118 |
+
"\n",
|
| 119 |
+
"Each tool internally calls the OpenEnv environment's standard `step()` method."
|
| 120 |
+
]
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"cell_type": "code",
|
| 124 |
+
"execution_count": null,
|
| 125 |
+
"metadata": {},
|
| 126 |
+
"outputs": [],
|
| 127 |
+
"source": "import random\nfrom typing import Optional\n\nfrom environment.config import GRID_SIZE, PROPERTY_NAMES, ATOM_TYPES\nfrom environment.rubrics import HeuristicRewardRubric\nfrom models import MaterialForgeAction, MaterialForgeObservation\nfrom server.material_forge_env_environment import MaterialForgeEnvironment\n\n\nDIFFICULTIES = [\"easy\", \"medium\", \"hard\"]\n\n\nclass MaterialForgeTRLEnv:\n \"\"\"TRL-compatible wrapper for MaterialForge crystal design.\"\"\"\n\n def __init__(self):\n self.env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n self.reward = 0.0\n self.done = False\n self._obs = None\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n\n def reset(self, **kwargs) -> str:\n seed = random.randint(0, 99999)\n difficulty = random.choice(DIFFICULTIES)\n obs = self.env.reset(seed=seed, difficulty=difficulty)\n self._obs = obs\n self.reward = 0.0\n self.done = False\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n return self._format_observation(obs)\n\n def place_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Place an atom on an empty cell of the crystal lattice.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: Atom species - 'A' (Metal, hardness, cost 8), 'B' (Conductor, conductivity, cost 6), 'C' (Ceramic, thermal resistance, cost 4), or 'P' (Polymer, elasticity, cost 2)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"place\", row, col, atom)\n\n def remove_atom(self, row: int, col: int) -> str:\n \"\"\"Remove an atom from the crystal lattice to free up budget.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"remove\", row, col, None)\n\n def replace_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Replace an existing atom with a different species to adjust material properties.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: New atom species - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"replace\", row, col, atom)\n\n def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:\n if self.done:\n raise ValueError(\"Episode is over. The crystal design session has ended.\")\n\n self._total_actions += 1\n row = max(0, min(int(row), GRID_SIZE - 1))\n col = max(0, min(int(col), GRID_SIZE - 1))\n\n if atom is not None:\n atom = str(atom).upper().strip()\n if atom not in ATOM_TYPES:\n atom = \"A\"\n\n # Pre-check: will this action actually change the grid?\n grid = self._obs.grid if self._obs else None\n if grid:\n cell = grid[row][col]\n if action_type == \"place\" and cell != \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot place at ({row},{col}) \u2014 cell is already \"\n f\"occupied by '{cell}'. Use replace_atom to change it, or pick an empty cell (marked '.').\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"remove\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot remove from ({row},{col}) \u2014 cell is empty. \"\n f\"Pick a cell that contains an atom.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot replace at ({row},{col}) \u2014 cell is empty. \"\n f\"Use place_atom instead.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == atom:\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cell ({row},{col}) already contains '{atom}'. \"\n f\"Pick a different atom species to replace it with.\\n\"\n + self._format_observation(self._obs)\n )\n\n action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)\n obs = self.env.step(action)\n self._obs = obs\n step_reward = obs.reward if obs.reward is not None else 0.0\n self._reward_history.append(step_reward)\n self._best_reward = max(self._best_reward, step_reward)\n self.reward = self._best_reward\n self.done = obs.done\n return self._format_observation(obs)\n\n def _format_observation(self, obs) -> str:\n grid_lines = []\n for r, row in enumerate(obs.grid):\n grid_lines.append(f\" {r}: {' '.join(cell if cell != '.' else '.' for cell in row)}\")\n grid_str = \"\\n\".join(grid_lines)\n\n gaps = {}\n for prop in PROPERTY_NAMES:\n t = obs.target.get(prop, 0.0)\n c = obs.current_properties.get(prop, 0.0)\n gaps[prop] = round(t - c, 1)\n\n props_str = \", \".join(\n f\"{p}={obs.current_properties.get(p, 0):.1f}/{obs.target.get(p, 0):.1f}(gap:{gaps[p]:+.1f})\"\n for p in PROPERTY_NAMES\n )\n\n sb = obs.score_breakdown or {}\n stability = sb.get(\"structural_stability\", 0.0)\n order = sb.get(\"lattice_order_index\", 0.0)\n\n return (\n f\"Step {obs.step_number}/{obs.max_steps} | \"\n f\"Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | \"\n f\"Phase: {obs.phase} | \"\n f\"Reward: {obs.reward:.4f}\\n\"\n f\"Properties: {props_str}\\n\"\n f\"Stability: {stability:.3f} | Order: {order:.3f}\\n\"\n f\"Grid (row: cols 0-7):\\n{grid_str}\"\n )\n\n\nprint(\"MaterialForgeTRLEnv defined.\")"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"cell_type": "markdown",
|
| 131 |
+
"metadata": {},
|
| 132 |
+
"source": [
|
| 133 |
+
"### Test the wrapper"
|
| 134 |
+
]
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"cell_type": "code",
|
| 138 |
+
"execution_count": null,
|
| 139 |
+
"metadata": {},
|
| 140 |
+
"outputs": [],
|
| 141 |
+
"source": [
|
| 142 |
+
"test_env = MaterialForgeTRLEnv()\n",
|
| 143 |
+
"init_obs = test_env.reset()\n",
|
| 144 |
+
"print(\"=== Initial State ===\")\n",
|
| 145 |
+
"print(init_obs)\n",
|
| 146 |
+
"\n",
|
| 147 |
+
"print(\"\\n=== After place_atom(3, 3, 'A') ===\")\n",
|
| 148 |
+
"result = test_env.place_atom(3, 3, \"A\")\n",
|
| 149 |
+
"print(result)\n",
|
| 150 |
+
"print(f\"\\nReward stored: {test_env.reward:.4f}\")\n",
|
| 151 |
+
"print(f\"Done: {test_env.done}\")\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"print(\"\\n=== After place_atom(3, 4, 'B') ===\")\n",
|
| 154 |
+
"result = test_env.place_atom(3, 4, \"B\")\n",
|
| 155 |
+
"print(result)\n",
|
| 156 |
+
"print(f\"Reward: {test_env.reward:.4f}\")"
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"cell_type": "markdown",
|
| 161 |
+
"metadata": {},
|
| 162 |
+
"source": [
|
| 163 |
+
"## 5. Reward Function"
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"cell_type": "code",
|
| 168 |
+
"execution_count": null,
|
| 169 |
+
"metadata": {},
|
| 170 |
+
"outputs": [],
|
| 171 |
+
"source": [
|
| 172 |
+
"def reward_func(environments, **kwargs) -> list[float]:\n \"\"\"Reward = best_reward + spatial_bonus + phase_bonus - invalid_penalty.\"\"\"\n rewards = []\n for env in environments:\n best = env._best_reward\n total = max(env._total_actions, 1)\n\n # Penalty for invalid actions\n invalid_ratio = env._invalid_actions / total\n invalid_penalty = 0.3 * invalid_ratio\n\n # Spatial diversity bonus: reward using multiple rows and columns\n spatial_bonus = 0.0\n if env._obs is not None:\n grid = env._obs.grid\n rows_used = set()\n cols_used = set()\n for r, row in enumerate(grid):\n for c, cell in enumerate(row):\n if cell != \".\":\n rows_used.add(r)\n cols_used.add(c)\n # Bonus for spreading across rows AND columns (max 0.10)\n row_spread = min(len(rows_used) / 4.0, 1.0) # 4+ rows = full bonus\n col_spread = min(len(cols_used) / 4.0, 1.0) # 4+ cols = full bonus\n spatial_bonus = 0.10 * (row_spread * col_spread)\n\n # Phase bonus\n phase_bonus = 0.0\n if env._obs is not None:\n phase = env._obs.phase\n if phase == \"crystalline\":\n phase_bonus = 0.15\n elif phase == \"polycrystalline\":\n phase_bonus = 0.05\n\n final = best + spatial_bonus + phase_bonus - invalid_penalty\n rewards.append(max(min(final, 1.0), 0.0))\n return rewards\n\nprint(\"Reward function: best_reward + spatial_bonus + phase_bonus - invalid_penalty\")"
|
| 173 |
+
]
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"cell_type": "markdown",
|
| 177 |
+
"metadata": {},
|
| 178 |
+
"source": [
|
| 179 |
+
"## 6. Training Dataset\n",
|
| 180 |
+
"\n",
|
| 181 |
+
"We create a prompt dataset where each entry instructs the LLM to design a crystal.\n",
|
| 182 |
+
"The environment generates random targets on each `reset()`, so every episode is unique."
|
| 183 |
+
]
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"cell_type": "code",
|
| 187 |
+
"execution_count": null,
|
| 188 |
+
"metadata": {},
|
| 189 |
+
"outputs": [],
|
| 190 |
+
"source": [
|
| 191 |
+
"from datasets import Dataset\n\nSYSTEM_PROMPT = \"\"\"You are an expert materials scientist designing crystal structures on an 8x8 atomic lattice.\n\nYour goal: place atoms to match target physical properties while staying within budget.\n\nAvailable atom species:\n- A (Metal): Primary contribution to hardness, cost 8 per atom\n- B (Conductor): Primary contribution to conductivity, cost 6 per atom\n- C (Ceramic): Primary contribution to thermal resistance, cost 4 per atom\n- P (Polymer): Primary contribution to elasticity, cost 2 per atom\n\nStrategy:\n- START from the center of the grid (around row 3-4, col 3-4) and expand outward in all directions\n- Build 2D clusters, NOT single rows or columns \u2014 atoms need neighbors in multiple directions for bonding\n- Group same-type atoms together in 2x2 or 3x3 blocks for maximum bonding bonuses\n- After each placement, READ the property gaps and choose the atom type that closes the LARGEST gap\n- Aim for crystalline phase: fill a compact rectangular region, not scattered atoms\n- Stay within the cost budget \u2014 use cheaper atoms (P=2, C=4) when budget is tight\n- Use replace_atom to fix overshooting properties, remove_atom if over budget\n\nUse the tools place_atom, remove_atom, and replace_atom to build your crystal.\"\"\"\n\nUSER_PROMPT = \"Design a crystal lattice that matches the target material properties. Start placing atoms from the center of the grid (row 3-4, col 3-4) and expand outward in all directions. Analyze the property gaps after each action and choose your next atom strategically.\"\n\nNUM_EPISODES = 200\n\ndataset = Dataset.from_dict({\n \"prompt\": [\n [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": USER_PROMPT},\n ]\n ] * NUM_EPISODES\n})\n\nprint(f\"Dataset created: {len(dataset)} episodes\")\nprint(f\"Sample prompt roles: {[m['role'] for m in dataset[0]['prompt']]}\")"
|
| 192 |
+
]
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"cell_type": "markdown",
|
| 196 |
+
"metadata": {},
|
| 197 |
+
"source": [
|
| 198 |
+
"## 7. Load Model with Unsloth\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"We use Unsloth for memory-efficient 4-bit QLoRA fine-tuning. Starting with `Qwen/Qwen3-0.6B` which fits on a free Colab T4 GPU. For better results with more compute, switch to `Qwen/Qwen3-1.7B` or `Qwen/Qwen2.5-3B-Instruct`."
|
| 201 |
+
]
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"cell_type": "code",
|
| 205 |
+
"execution_count": null,
|
| 206 |
+
"metadata": {},
|
| 207 |
+
"outputs": [],
|
| 208 |
+
"source": "import torch\nfrom unsloth import FastLanguageModel\n\nMODEL_NAME = \"Qwen/Qwen3-0.6B\" # Change to Qwen3-1.7B or Qwen2.5-3B-Instruct for better results\nMAX_SEQ_LENGTH = 4096\n\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n MODEL_NAME,\n max_seq_length=MAX_SEQ_LENGTH,\n load_in_4bit=True,\n dtype=None,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n model,\n r=16,\n lora_alpha=16,\n lora_dropout=0,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n \"gate_proj\", \"up_proj\", \"down_proj\"],\n use_gradient_checkpointing=\"unsloth\",\n)\n\n# --- Fix Unsloth + TRL compatibility for Qwen3 tool calling ---\n# Unsloth modifies the chat template, breaking TRL's string-comparison checks.\n# We load TRL's own Qwen3 training template (prefix-preserving, has {% generation %}\n# markers) and response schema directly.\nfrom pathlib import Path\nimport trl.chat_template_utils as _ctu\n\n# 1) Set the response schema so TRL can parse <tool_call> blocks\ntokenizer.response_schema = {\n \"x-regex\": r\"^(?:<think>\\n?(?:(?P<reasoning_content>.*?\\S.*?)\\n?|[\\s]*)</think>\\s*)?(?P<content>.*?)(?:\\n(?=<tool_call>))?(?=(?:<tool_call>|<\\|im_end\\|>|$))(?P<tool_calls>(?:<tool_call>.+?</tool_call>\\s*)+)?\\s*(?:<\\|im_end\\|>|$)\",\n \"type\": \"object\",\n \"properties\": {\n \"role\": {\"const\": \"assistant\"},\n \"content\": {\"type\": \"string\"},\n \"reasoning_content\": {\"type\": \"string\"},\n \"tool_calls\": {\n \"type\": \"array\",\n \"x-regex-iterator\": r\"<tool_call>\\s*(.+?)\\s*</tool_call>\",\n \"items\": {\n \"x-parser\": \"json\",\n \"x-parser-args\": {\"transform\": \"{type: 'function', function: @}\"},\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\"const\": \"function\"},\n \"function\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\"type\": \"string\"},\n \"arguments\": {\"type\": \"object\", \"additionalProperties\": {}},\n },\n },\n },\n },\n },\n },\n}\n\n# 2) Replace Unsloth's modified template with TRL's training-safe Qwen3 template\n_templates_dir = Path(_ctu.__file__).parent / \"chat_templates\"\ntokenizer.chat_template = (_templates_dir / \"qwen3_training.jinja\").read_text()\n\nprint(f\"Model loaded: {MODEL_NAME}\")\nprint(f\"Trainable params: {model.print_trainable_parameters()}\")\nprint(f\"Chat template: TRL qwen3_training.jinja (prefix-preserving)\")\nprint(f\"Response schema: Qwen3 tool-call parser\")"
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"cell_type": "markdown",
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"source": [
|
| 214 |
+
"## 8. GRPO Training\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"The `GRPOTrainer` with `environment_factory` handles the full multi-turn loop:\n",
|
| 217 |
+
"1. Generates completions from the LLM\n",
|
| 218 |
+
"2. Parses tool calls (`place_atom`, `remove_atom`, `replace_atom`)\n",
|
| 219 |
+
"3. Executes them against the MaterialForge environment\n",
|
| 220 |
+
"4. Feeds observations back to the model\n",
|
| 221 |
+
"5. Collects rewards and updates the policy via GRPO"
|
| 222 |
+
]
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"cell_type": "code",
|
| 226 |
+
"execution_count": null,
|
| 227 |
+
"metadata": {},
|
| 228 |
+
"outputs": [],
|
| 229 |
+
"source": [
|
| 230 |
+
"from trl import GRPOConfig, GRPOTrainer\n\nOUTPUT_DIR = \"materialforge-grpo-checkpoints\"\n\ntraining_args = GRPOConfig(\n output_dir=OUTPUT_DIR,\n\n # Generation\n num_generations=4,\n max_completion_length=2048,\n temperature=1.0,\n\n # Optimization\n learning_rate=5e-5,\n per_device_train_batch_size=1,\n gradient_accumulation_steps=4,\n num_train_epochs=1,\n\n # Logging\n logging_steps=1,\n log_completions=True,\n num_completions_to_print=1,\n\n # Tool calling\n chat_template_kwargs={\"enable_thinking\": False},\n\n # Saving\n save_steps=50,\n save_total_limit=2,\n)\n\ntrainer = GRPOTrainer(\n model=model,\n processing_class=tokenizer,\n reward_funcs=reward_func,\n train_dataset=dataset,\n args=training_args,\n environment_factory=MaterialForgeTRLEnv,\n)\n\nprint(\"Trainer initialized. Starting GRPO training...\")\nprint(f\" Episodes: {NUM_EPISODES}\")\nprint(f\" Generations per prompt: {training_args.num_generations}\")\nprint(f\" Max completion length: {training_args.max_completion_length}\")\nprint(f\" Temperature: {training_args.temperature}\")\nprint(f\" Learning rate: {training_args.learning_rate}\")\nprint(f\" Gradient accumulation: {training_args.gradient_accumulation_steps}\")\nprint(f\" Total steps: {NUM_EPISODES // (training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps)}\")"
|
| 231 |
+
]
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"cell_type": "code",
|
| 235 |
+
"execution_count": null,
|
| 236 |
+
"metadata": {},
|
| 237 |
+
"outputs": [],
|
| 238 |
+
"source": [
|
| 239 |
+
"train_result = trainer.train()\n",
|
| 240 |
+
"print(f\"\\nTraining complete!\")\n",
|
| 241 |
+
"print(f\" Total steps: {trainer.state.global_step}\")\n",
|
| 242 |
+
"print(f\" Final loss: {train_result.training_loss:.4f}\")"
|
| 243 |
+
]
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"cell_type": "markdown",
|
| 247 |
+
"metadata": {},
|
| 248 |
+
"source": [
|
| 249 |
+
"## 9. Save Trained Model"
|
| 250 |
+
]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"cell_type": "code",
|
| 254 |
+
"execution_count": null,
|
| 255 |
+
"metadata": {},
|
| 256 |
+
"outputs": [],
|
| 257 |
+
"source": [
|
| 258 |
+
"SAVE_DIR = \"materialforge-grpo-trained\"\n",
|
| 259 |
+
"trainer.save_model(SAVE_DIR)\n",
|
| 260 |
+
"tokenizer.save_pretrained(SAVE_DIR)\n",
|
| 261 |
+
"print(f\"Model saved to {SAVE_DIR}/\")"
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"cell_type": "markdown",
|
| 266 |
+
"metadata": {},
|
| 267 |
+
"source": [
|
| 268 |
+
"## 10. Training Reward Curves\n",
|
| 269 |
+
"\n",
|
| 270 |
+
"Extract and plot the reward history from training logs. These plots are a **required deliverable** \u2014 they must be saved as images and embedded in the README."
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"cell_type": "code",
|
| 275 |
+
"execution_count": null,
|
| 276 |
+
"metadata": {},
|
| 277 |
+
"outputs": [],
|
| 278 |
+
"source": [
|
| 279 |
+
"import matplotlib.pyplot as plt\n",
|
| 280 |
+
"import json\n",
|
| 281 |
+
"\n",
|
| 282 |
+
"os.makedirs(\"plots\", exist_ok=True)\n",
|
| 283 |
+
"\n",
|
| 284 |
+
"log_history = trainer.state.log_history\n",
|
| 285 |
+
"\n",
|
| 286 |
+
"steps = []\n",
|
| 287 |
+
"rewards = []\n",
|
| 288 |
+
"losses = []\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"for entry in log_history:\n",
|
| 291 |
+
" if \"reward\" in entry:\n",
|
| 292 |
+
" steps.append(entry.get(\"step\", len(steps)))\n",
|
| 293 |
+
" rewards.append(entry[\"reward\"])\n",
|
| 294 |
+
" if \"loss\" in entry:\n",
|
| 295 |
+
" if len(losses) < len(steps):\n",
|
| 296 |
+
" losses.append(entry[\"loss\"])\n",
|
| 297 |
+
"\n",
|
| 298 |
+
"# --- Plot 1: Reward over training ---\n",
|
| 299 |
+
"fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 300 |
+
"ax.plot(steps[:len(rewards)], rewards, color=\"#4285f4\", linewidth=2, label=\"GRPO Reward\")\n",
|
| 301 |
+
"if len(rewards) > 5:\n",
|
| 302 |
+
" window = max(3, len(rewards) // 10)\n",
|
| 303 |
+
" smoothed = [sum(rewards[max(0,i-window):i+1])/len(rewards[max(0,i-window):i+1]) for i in range(len(rewards))]\n",
|
| 304 |
+
" ax.plot(steps[:len(smoothed)], smoothed, color=\"#ea4335\", linewidth=2, linestyle=\"--\", label=f\"Smoothed (window={window})\")\n",
|
| 305 |
+
"ax.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 306 |
+
"ax.set_ylabel(\"Reward\", fontsize=12)\n",
|
| 307 |
+
"ax.set_title(\"MaterialForge GRPO Training \u2014 Reward Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 308 |
+
"ax.legend()\n",
|
| 309 |
+
"ax.grid(True, alpha=0.3)\n",
|
| 310 |
+
"fig.tight_layout()\n",
|
| 311 |
+
"fig.savefig(\"plots/reward_curve.png\", dpi=150)\n",
|
| 312 |
+
"plt.show()\n",
|
| 313 |
+
"print(\"Saved: plots/reward_curve.png\")\n",
|
| 314 |
+
"\n",
|
| 315 |
+
"# --- Plot 2: Loss over training ---\n",
|
| 316 |
+
"if losses:\n",
|
| 317 |
+
" fig2, ax2 = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 318 |
+
" ax2.plot(range(len(losses)), losses, color=\"#34a853\", linewidth=2)\n",
|
| 319 |
+
" ax2.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 320 |
+
" ax2.set_ylabel(\"Loss\", fontsize=12)\n",
|
| 321 |
+
" ax2.set_title(\"MaterialForge GRPO Training \u2014 Loss Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 322 |
+
" ax2.grid(True, alpha=0.3)\n",
|
| 323 |
+
" fig2.tight_layout()\n",
|
| 324 |
+
" fig2.savefig(\"plots/loss_curve.png\", dpi=150)\n",
|
| 325 |
+
" plt.show()\n",
|
| 326 |
+
" print(\"Saved: plots/loss_curve.png\")"
|
| 327 |
+
]
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"cell_type": "markdown",
|
| 331 |
+
"metadata": {},
|
| 332 |
+
"source": [
|
| 333 |
+
"## 11. Baseline Comparison\n",
|
| 334 |
+
"\n",
|
| 335 |
+
"Compare the trained agent against a random baseline to demonstrate improvement."
|
| 336 |
+
]
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"cell_type": "code",
|
| 340 |
+
"execution_count": null,
|
| 341 |
+
"metadata": {},
|
| 342 |
+
"outputs": [],
|
| 343 |
+
"source": [
|
| 344 |
+
"import random as rng\n",
|
| 345 |
+
"\n",
|
| 346 |
+
"def run_random_baseline(n_episodes=20, max_steps=30):\n",
|
| 347 |
+
" \"\"\"Run a random agent as baseline.\"\"\"\n",
|
| 348 |
+
" env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n",
|
| 349 |
+
" episode_rewards = []\n",
|
| 350 |
+
" episode_best = []\n",
|
| 351 |
+
"\n",
|
| 352 |
+
" for ep in range(n_episodes):\n",
|
| 353 |
+
" obs = env.reset(seed=ep * 7, difficulty=\"easy\")\n",
|
| 354 |
+
" rewards = []\n",
|
| 355 |
+
" for step in range(max_steps):\n",
|
| 356 |
+
" action_type = rng.choice([\"place\", \"place\", \"place\", \"replace\", \"remove\"])\n",
|
| 357 |
+
" row = rng.randint(0, 7)\n",
|
| 358 |
+
" col = rng.randint(0, 7)\n",
|
| 359 |
+
" atom = rng.choice([\"A\", \"B\", \"C\", \"P\"]) if action_type != \"remove\" else None\n",
|
| 360 |
+
" action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)\n",
|
| 361 |
+
" obs = env.step(action)\n",
|
| 362 |
+
" rewards.append(obs.reward if obs.reward else 0.0)\n",
|
| 363 |
+
" if obs.done:\n",
|
| 364 |
+
" break\n",
|
| 365 |
+
" episode_rewards.append(sum(rewards) / len(rewards) if rewards else 0.0)\n",
|
| 366 |
+
" episode_best.append(max(rewards) if rewards else 0.0)\n",
|
| 367 |
+
"\n",
|
| 368 |
+
" return episode_rewards, episode_best\n",
|
| 369 |
+
"\n",
|
| 370 |
+
"print(\"Running random baseline (20 episodes)...\")\n",
|
| 371 |
+
"random_avg_rewards, random_best_rewards = run_random_baseline(20)\n",
|
| 372 |
+
"print(f\"Random Agent \u2014 Mean reward: {sum(random_avg_rewards)/len(random_avg_rewards):.4f}\")\n",
|
| 373 |
+
"print(f\"Random Agent \u2014 Mean best: {sum(random_best_rewards)/len(random_best_rewards):.4f}\")"
|
| 374 |
+
]
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"cell_type": "code",
|
| 378 |
+
"execution_count": null,
|
| 379 |
+
"metadata": {},
|
| 380 |
+
"outputs": [],
|
| 381 |
+
"source": [
|
| 382 |
+
"# Comparison bar chart\n",
|
| 383 |
+
"trained_mean_reward = sum(rewards) / len(rewards) if rewards else 0.0\n",
|
| 384 |
+
"random_mean_reward = sum(random_avg_rewards) / len(random_avg_rewards)\n",
|
| 385 |
+
"\n",
|
| 386 |
+
"fig3, ax3 = plt.subplots(figsize=(8, 5))\n",
|
| 387 |
+
"agents = [\"Random\\nBaseline\", \"GRPO-Trained\\nAgent\"]\n",
|
| 388 |
+
"means = [random_mean_reward, trained_mean_reward]\n",
|
| 389 |
+
"colors = [\"#ea4335\", \"#4285f4\"]\n",
|
| 390 |
+
"bars = ax3.bar(agents, means, color=colors, width=0.5, edgecolor=\"white\", linewidth=1.5)\n",
|
| 391 |
+
"\n",
|
| 392 |
+
"for bar, val in zip(bars, means):\n",
|
| 393 |
+
" ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n",
|
| 394 |
+
" f\"{val:.4f}\", ha=\"center\", va=\"bottom\", fontweight=\"bold\", fontsize=13)\n",
|
| 395 |
+
"\n",
|
| 396 |
+
"ax3.set_ylabel(\"Mean Reward\", fontsize=12)\n",
|
| 397 |
+
"ax3.set_title(\"MaterialForge \u2014 Random Baseline vs GRPO-Trained Agent\", fontsize=14, fontweight=\"bold\")\n",
|
| 398 |
+
"ax3.set_ylim(0, max(means) * 1.3 + 0.05)\n",
|
| 399 |
+
"ax3.grid(axis=\"y\", alpha=0.3)\n",
|
| 400 |
+
"fig3.tight_layout()\n",
|
| 401 |
+
"fig3.savefig(\"plots/baseline_comparison.png\", dpi=150)\n",
|
| 402 |
+
"plt.show()\n",
|
| 403 |
+
"print(\"Saved: plots/baseline_comparison.png\")"
|
| 404 |
+
]
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"cell_type": "markdown",
|
| 408 |
+
"metadata": {},
|
| 409 |
+
"source": [
|
| 410 |
+
"## 12. Trained Agent Inference Demo\n",
|
| 411 |
+
"\n",
|
| 412 |
+
"Run the trained model on a fresh episode to see its crystal design strategy."
|
| 413 |
+
]
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"cell_type": "code",
|
| 417 |
+
"execution_count": null,
|
| 418 |
+
"metadata": {},
|
| 419 |
+
"outputs": [],
|
| 420 |
+
"source": [
|
| 421 |
+
"from transformers import AutoTokenizer, AutoModelForCausalLM\n",
|
| 422 |
+
"\n",
|
| 423 |
+
"print(\"Loading trained model for inference...\")\n",
|
| 424 |
+
"inf_model, inf_tokenizer = FastLanguageModel.from_pretrained(\n",
|
| 425 |
+
" SAVE_DIR,\n",
|
| 426 |
+
" max_seq_length=MAX_SEQ_LENGTH,\n",
|
| 427 |
+
" load_in_4bit=True,\n",
|
| 428 |
+
")\n",
|
| 429 |
+
"FastLanguageModel.for_inference(inf_model)\n",
|
| 430 |
+
"\n",
|
| 431 |
+
"demo_env = MaterialForgeTRLEnv()\n",
|
| 432 |
+
"obs_text = demo_env.reset()\n",
|
| 433 |
+
"\n",
|
| 434 |
+
"messages = [\n",
|
| 435 |
+
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 436 |
+
" {\"role\": \"user\", \"content\": USER_PROMPT},\n",
|
| 437 |
+
" {\"role\": \"assistant\", \"content\": f\"Let me analyze the initial state:\\n{obs_text}\\n\\nI'll start placing atoms strategically.\"},\n",
|
| 438 |
+
"]\n",
|
| 439 |
+
"\n",
|
| 440 |
+
"print(\"=\" * 60)\n",
|
| 441 |
+
"print(\"TRAINED AGENT DEMO\")\n",
|
| 442 |
+
"print(\"=\" * 60)\n",
|
| 443 |
+
"print(f\"\\nInitial observation:\\n{obs_text}\")\n",
|
| 444 |
+
"print(f\"\\nAgent designing crystal...\")\n",
|
| 445 |
+
"print(f\"(Full multi-turn inference requires TRL's tool-call loop.\")\n",
|
| 446 |
+
"print(f\"See the training logs above for actual agent behavior.)\")"
|
| 447 |
+
]
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"cell_type": "markdown",
|
| 451 |
+
"metadata": {},
|
| 452 |
+
"source": [
|
| 453 |
+
"## 13. Summary\n",
|
| 454 |
+
"\n",
|
| 455 |
+
"| Metric | Random Baseline | GRPO-Trained Agent |\n",
|
| 456 |
+
"|--------|----------------|--------------------|\n",
|
| 457 |
+
"| Mean Reward | *see above* | *see above* |\n",
|
| 458 |
+
"| Phase Achieved | Amorphous | *see training logs* |\n",
|
| 459 |
+
"\n",
|
| 460 |
+
"### Deliverables produced by this notebook:\n",
|
| 461 |
+
"- `plots/reward_curve.png` \u2014 Reward over training steps\n",
|
| 462 |
+
"- `plots/loss_curve.png` \u2014 Training loss curve\n",
|
| 463 |
+
"- `plots/baseline_comparison.png` \u2014 Random vs trained comparison\n",
|
| 464 |
+
"- `materialforge-grpo-trained/` \u2014 Saved model checkpoint\n",
|
| 465 |
+
"\n",
|
| 466 |
+
"### Links\n",
|
| 467 |
+
"- **Live Environment**: [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env)\n",
|
| 468 |
+
"- **Repository**: [MaterialForge GitHub](https://github.com/Arsh-Pathan/MaterialForge)"
|
| 469 |
+
]
|
| 470 |
+
}
|
| 471 |
+
],
|
| 472 |
+
"metadata": {
|
| 473 |
+
"accelerator": "GPU",
|
| 474 |
+
"colab": {
|
| 475 |
+
"gpuType": "T4",
|
| 476 |
+
"provenance": [],
|
| 477 |
+
"name": "MaterialForge_GRPO_Training.ipynb"
|
| 478 |
+
},
|
| 479 |
+
"kernelspec": {
|
| 480 |
+
"display_name": "Python 3",
|
| 481 |
+
"name": "python3"
|
| 482 |
+
},
|
| 483 |
+
"language_info": {
|
| 484 |
+
"name": "python",
|
| 485 |
+
"version": "3.11.0"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"nbformat": 4,
|
| 489 |
+
"nbformat_minor": 0
|
| 490 |
+
}
|
training/Traning Runs/Run - II/baseline_comparison.png
ADDED
|
training/Traning Runs/Run - II/loss_curve.png
ADDED
|
training/Traning Runs/Run - II/reward_curve.png
ADDED
|
training/Traning Runs/Run - III/MaterialForge_GRPO_Training.ipynb
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# MaterialForge: GRPO Training for Crystal Design\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"**Meta PyTorch OpenEnv Hackathon x Scaler School of Technology \u2014 Grand Finale**\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"This notebook trains an LLM to autonomously design crystalline structures using reinforcement learning (GRPO) with the MaterialForge environment.\n",
|
| 12 |
+
"\n",
|
| 13 |
+
"| Component | Details |\n",
|
| 14 |
+
"|-----------|----------|\n",
|
| 15 |
+
"| **Environment** | MaterialForge \u2014 8x8 atomic lattice, 4 atom species, 4 physical properties |\n",
|
| 16 |
+
"| **Algorithm** | GRPO (Group Relative Policy Optimization) via HuggingFace TRL |\n",
|
| 17 |
+
"| **Efficiency** | Unsloth for 4-bit QLoRA training |\n",
|
| 18 |
+
"| **Framework** | OpenEnv-compliant environment |\n",
|
| 19 |
+
"| **HF Space** | [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env) |"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cell_type": "markdown",
|
| 24 |
+
"metadata": {},
|
| 25 |
+
"source": [
|
| 26 |
+
"## 1. Installation"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"cell_type": "code",
|
| 31 |
+
"metadata": {
|
| 32 |
+
"jupyter": {
|
| 33 |
+
"is_executing": true
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"source": "%%capture\n!pip install -Uq unsloth\n!pip install -Uq trl>=0.18.0 transformers datasets accelerate peft\n!pip install -Uq \"openenv-core[core]>=0.2.3\" aiofiles openai python-dotenv\n!pip install -Uq matplotlib jmespath",
|
| 37 |
+
"outputs": [],
|
| 38 |
+
"execution_count": null
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "markdown",
|
| 42 |
+
"metadata": {},
|
| 43 |
+
"source": [
|
| 44 |
+
"## 2. Clone Environment & Setup Paths\n",
|
| 45 |
+
"\n",
|
| 46 |
+
"We clone the MaterialForge repository to access the environment code directly.\n",
|
| 47 |
+
"The same environment is hosted live at: https://huggingface.co/spaces/ArshPathan/material_forge_env"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"cell_type": "code",
|
| 52 |
+
"execution_count": null,
|
| 53 |
+
"metadata": {},
|
| 54 |
+
"outputs": [],
|
| 55 |
+
"source": [
|
| 56 |
+
"import os\n",
|
| 57 |
+
"\n",
|
| 58 |
+
"REPO_URL = \"https://huggingface.co/spaces/ArshPathan/material_forge_env\"\n",
|
| 59 |
+
"REPO_DIR = \"MaterialForge\"\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"if not os.path.isdir(REPO_DIR):\n",
|
| 62 |
+
" !git clone {REPO_URL} {REPO_DIR}\n",
|
| 63 |
+
"else:\n",
|
| 64 |
+
" print(f\"{REPO_DIR}/ already exists, skipping clone.\")\n",
|
| 65 |
+
"\n",
|
| 66 |
+
"import sys\n",
|
| 67 |
+
"sys.path.insert(0, REPO_DIR)\n",
|
| 68 |
+
"\n",
|
| 69 |
+
"print(\"Environment source ready.\")"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"cell_type": "markdown",
|
| 74 |
+
"metadata": {},
|
| 75 |
+
"source": [
|
| 76 |
+
"## 3. Verify Environment Works\n",
|
| 77 |
+
"\n",
|
| 78 |
+
"Quick sanity check: reset the environment, take one action, confirm reward is computed."
|
| 79 |
+
]
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"cell_type": "code",
|
| 83 |
+
"execution_count": null,
|
| 84 |
+
"metadata": {},
|
| 85 |
+
"outputs": [],
|
| 86 |
+
"source": [
|
| 87 |
+
"from environment.rubrics import HeuristicRewardRubric\n",
|
| 88 |
+
"from server.material_forge_env_environment import MaterialForgeEnvironment\n",
|
| 89 |
+
"from models import MaterialForgeAction\n",
|
| 90 |
+
"\n",
|
| 91 |
+
"env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n",
|
| 92 |
+
"obs = env.reset(seed=42, difficulty=\"easy\")\n",
|
| 93 |
+
"print(f\"Target properties: {obs.target}\")\n",
|
| 94 |
+
"print(f\"Cost budget: {obs.cost_budget}\")\n",
|
| 95 |
+
"print(f\"Max steps: {obs.max_steps}\")\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"action = MaterialForgeAction(action_type=\"place\", row=3, col=3, atom=\"A\")\n",
|
| 98 |
+
"obs = env.step(action)\n",
|
| 99 |
+
"print(f\"\\nAfter placing Metal at (3,3):\")\n",
|
| 100 |
+
"print(f\" Reward: {obs.reward}\")\n",
|
| 101 |
+
"print(f\" Phase: {obs.phase}\")\n",
|
| 102 |
+
"print(f\" Properties: {obs.current_properties}\")\n",
|
| 103 |
+
"print(\"\\nEnvironment OK!\")"
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"cell_type": "markdown",
|
| 108 |
+
"metadata": {},
|
| 109 |
+
"source": [
|
| 110 |
+
"## 4. TRL Environment Wrapper\n",
|
| 111 |
+
"\n",
|
| 112 |
+
"TRL's `GRPOTrainer` with `environment_factory` requires a wrapper class that exposes **named tool methods** (not a generic `step()`). The trainer discovers these methods and presents them to the LLM as callable tools.\n",
|
| 113 |
+
"\n",
|
| 114 |
+
"Our wrapper exposes three tools:\n",
|
| 115 |
+
"- `place_atom(row, col, atom)` \u2014 place a new atom on an empty cell\n",
|
| 116 |
+
"- `remove_atom(row, col)` \u2014 remove an existing atom\n",
|
| 117 |
+
"- `replace_atom(row, col, atom)` \u2014 swap an atom for a different species\n",
|
| 118 |
+
"\n",
|
| 119 |
+
"Each tool internally calls the OpenEnv environment's standard `step()` method."
|
| 120 |
+
]
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"cell_type": "code",
|
| 124 |
+
"execution_count": null,
|
| 125 |
+
"metadata": {},
|
| 126 |
+
"outputs": [],
|
| 127 |
+
"source": "import random\nfrom typing import Optional\n\nfrom environment.config import GRID_SIZE, PROPERTY_NAMES, ATOM_TYPES\nfrom environment.rubrics import HeuristicRewardRubric\nfrom models import MaterialForgeAction, MaterialForgeObservation\nfrom server.material_forge_env_environment import MaterialForgeEnvironment\n\n\nDIFFICULTIES = [\"easy\", \"medium\", \"hard\"]\n\n\nclass MaterialForgeTRLEnv:\n \"\"\"TRL-compatible wrapper for MaterialForge crystal design.\"\"\"\n\n def __init__(self):\n self.env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n self.reward = 0.0\n self.done = False\n self._obs = None\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n\n def reset(self, **kwargs) -> str:\n seed = random.randint(0, 99999)\n difficulty = random.choice(DIFFICULTIES)\n obs = self.env.reset(seed=seed, difficulty=difficulty)\n self._obs = obs\n self.reward = 0.0\n self.done = False\n self._best_reward = 0.0\n self._reward_history = []\n self._invalid_actions = 0\n self._total_actions = 0\n return self._format_observation(obs)\n\n def place_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Place an atom on an empty cell of the crystal lattice.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: Atom species - 'A' (Metal, hardness, cost 8), 'B' (Conductor, conductivity, cost 6), 'C' (Ceramic, thermal resistance, cost 4), or 'P' (Polymer, elasticity, cost 2)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"place\", row, col, atom)\n\n def remove_atom(self, row: int, col: int) -> str:\n \"\"\"Remove an atom from the crystal lattice to free up budget.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"remove\", row, col, None)\n\n def replace_atom(self, row: int, col: int, atom: str) -> str:\n \"\"\"Replace an existing atom with a different species to adjust material properties.\n\n Args:\n row: Row index on the 8x8 grid (0-7)\n col: Column index on the 8x8 grid (0-7)\n atom: New atom species - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)\n\n Returns:\n Updated lattice state with current properties, target gaps, and phase.\n \"\"\"\n return self._do_step(\"replace\", row, col, atom)\n\n def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:\n if self.done:\n raise ValueError(\"Episode is over. The crystal design session has ended.\")\n\n self._total_actions += 1\n row = max(0, min(int(row), GRID_SIZE - 1))\n col = max(0, min(int(col), GRID_SIZE - 1))\n\n if atom is not None:\n atom = str(atom).upper().strip()\n if atom not in ATOM_TYPES:\n atom = \"A\"\n\n # Pre-check: will this action actually change the grid?\n grid = self._obs.grid if self._obs else None\n if grid:\n cell = grid[row][col]\n if action_type == \"place\" and cell != \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot place at ({row},{col}) \u2014 cell is already \"\n f\"occupied by '{cell}'. Use replace_atom to change it, or pick an empty cell (marked '.').\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"remove\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot remove from ({row},{col}) \u2014 cell is empty. \"\n f\"Pick a cell that contains an atom.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == \".\":\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cannot replace at ({row},{col}) \u2014 cell is empty. \"\n f\"Use place_atom instead.\\n\"\n + self._format_observation(self._obs)\n )\n if action_type == \"replace\" and cell == atom:\n self._invalid_actions += 1\n return (\n f\"INVALID ACTION: Cell ({row},{col}) already contains '{atom}'. \"\n f\"Pick a different atom species to replace it with.\\n\"\n + self._format_observation(self._obs)\n )\n\n action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)\n obs = self.env.step(action)\n self._obs = obs\n step_reward = obs.reward if obs.reward is not None else 0.0\n self._reward_history.append(step_reward)\n self._best_reward = max(self._best_reward, step_reward)\n self.reward = self._best_reward\n self.done = obs.done\n return self._format_observation(obs)\n\n def _format_observation(self, obs) -> str:\n grid_lines = []\n for r, row in enumerate(obs.grid):\n grid_lines.append(f\" {r}: {' '.join(cell if cell != '.' else '.' for cell in row)}\")\n grid_str = \"\\n\".join(grid_lines)\n\n gaps = {}\n for prop in PROPERTY_NAMES:\n t = obs.target.get(prop, 0.0)\n c = obs.current_properties.get(prop, 0.0)\n gaps[prop] = round(t - c, 1)\n\n props_str = \", \".join(\n f\"{p}={obs.current_properties.get(p, 0):.1f}/{obs.target.get(p, 0):.1f}(gap:{gaps[p]:+.1f})\"\n for p in PROPERTY_NAMES\n )\n\n sb = obs.score_breakdown or {}\n stability = sb.get(\"structural_stability\", 0.0)\n order = sb.get(\"lattice_order_index\", 0.0)\n\n return (\n f\"Step {obs.step_number}/{obs.max_steps} | \"\n f\"Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | \"\n f\"Phase: {obs.phase} | \"\n f\"Reward: {obs.reward:.4f}\\n\"\n f\"Properties: {props_str}\\n\"\n f\"Stability: {stability:.3f} | Order: {order:.3f}\\n\"\n f\"Grid (row: cols 0-7):\\n{grid_str}\"\n )\n\n\nprint(\"MaterialForgeTRLEnv defined.\")"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"cell_type": "markdown",
|
| 131 |
+
"metadata": {},
|
| 132 |
+
"source": [
|
| 133 |
+
"### Test the wrapper"
|
| 134 |
+
]
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"cell_type": "code",
|
| 138 |
+
"execution_count": null,
|
| 139 |
+
"metadata": {},
|
| 140 |
+
"outputs": [],
|
| 141 |
+
"source": [
|
| 142 |
+
"test_env = MaterialForgeTRLEnv()\n",
|
| 143 |
+
"init_obs = test_env.reset()\n",
|
| 144 |
+
"print(\"=== Initial State ===\")\n",
|
| 145 |
+
"print(init_obs)\n",
|
| 146 |
+
"\n",
|
| 147 |
+
"print(\"\\n=== After place_atom(3, 3, 'A') ===\")\n",
|
| 148 |
+
"result = test_env.place_atom(3, 3, \"A\")\n",
|
| 149 |
+
"print(result)\n",
|
| 150 |
+
"print(f\"\\nReward stored: {test_env.reward:.4f}\")\n",
|
| 151 |
+
"print(f\"Done: {test_env.done}\")\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"print(\"\\n=== After place_atom(3, 4, 'B') ===\")\n",
|
| 154 |
+
"result = test_env.place_atom(3, 4, \"B\")\n",
|
| 155 |
+
"print(result)\n",
|
| 156 |
+
"print(f\"Reward: {test_env.reward:.4f}\")"
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"cell_type": "markdown",
|
| 161 |
+
"metadata": {},
|
| 162 |
+
"source": [
|
| 163 |
+
"## 5. Reward Function"
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"cell_type": "code",
|
| 168 |
+
"execution_count": null,
|
| 169 |
+
"metadata": {},
|
| 170 |
+
"outputs": [],
|
| 171 |
+
"source": [
|
| 172 |
+
"def reward_func(environments, **kwargs) -> list[float]:\n \"\"\"Reward = best_reward + spatial_bonus + phase_bonus - invalid_penalty.\"\"\"\n rewards = []\n for env in environments:\n best = env._best_reward\n total = max(env._total_actions, 1)\n\n # Penalty for invalid actions\n invalid_ratio = env._invalid_actions / total\n invalid_penalty = 0.3 * invalid_ratio\n\n # Spatial diversity bonus: reward using multiple rows and columns\n spatial_bonus = 0.0\n if env._obs is not None:\n grid = env._obs.grid\n rows_used = set()\n cols_used = set()\n for r, row in enumerate(grid):\n for c, cell in enumerate(row):\n if cell != \".\":\n rows_used.add(r)\n cols_used.add(c)\n # Bonus for spreading across rows AND columns (max 0.10)\n row_spread = min(len(rows_used) / 4.0, 1.0) # 4+ rows = full bonus\n col_spread = min(len(cols_used) / 4.0, 1.0) # 4+ cols = full bonus\n spatial_bonus = 0.10 * (row_spread * col_spread)\n\n # Phase bonus\n phase_bonus = 0.0\n if env._obs is not None:\n phase = env._obs.phase\n if phase == \"crystalline\":\n phase_bonus = 0.15\n elif phase == \"polycrystalline\":\n phase_bonus = 0.05\n\n final = best + spatial_bonus + phase_bonus - invalid_penalty\n rewards.append(max(min(final, 1.0), 0.0))\n return rewards\n\nprint(\"Reward function: best_reward + spatial_bonus + phase_bonus - invalid_penalty\")"
|
| 173 |
+
]
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"cell_type": "markdown",
|
| 177 |
+
"metadata": {},
|
| 178 |
+
"source": [
|
| 179 |
+
"## 6. Training Dataset\n",
|
| 180 |
+
"\n",
|
| 181 |
+
"We create a prompt dataset where each entry instructs the LLM to design a crystal.\n",
|
| 182 |
+
"The environment generates random targets on each `reset()`, so every episode is unique."
|
| 183 |
+
]
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"cell_type": "code",
|
| 187 |
+
"execution_count": null,
|
| 188 |
+
"metadata": {},
|
| 189 |
+
"outputs": [],
|
| 190 |
+
"source": [
|
| 191 |
+
"from datasets import Dataset\n\nSYSTEM_PROMPT = \"\"\"You are an expert materials scientist designing crystal structures on an 8x8 atomic lattice.\n\nYour goal: place atoms to match target physical properties while staying within budget.\n\nAvailable atom species:\n- A (Metal): Primary contribution to hardness, cost 8 per atom\n- B (Conductor): Primary contribution to conductivity, cost 6 per atom\n- C (Ceramic): Primary contribution to thermal resistance, cost 4 per atom\n- P (Polymer): Primary contribution to elasticity, cost 2 per atom\n\nStrategy:\n- START from the center of the grid (around row 3-4, col 3-4) and expand outward in all directions\n- Build 2D clusters, NOT single rows or columns \u2014 atoms need neighbors in multiple directions for bonding\n- Group same-type atoms together in 2x2 or 3x3 blocks for maximum bonding bonuses\n- After each placement, READ the property gaps and choose the atom type that closes the LARGEST gap\n- Aim for crystalline phase: fill a compact rectangular region, not scattered atoms\n- Stay within the cost budget \u2014 use cheaper atoms (P=2, C=4) when budget is tight\n- Use replace_atom to fix overshooting properties, remove_atom if over budget\n\nUse the tools place_atom, remove_atom, and replace_atom to build your crystal.\"\"\"\n\nUSER_PROMPT = \"Design a crystal lattice that matches the target material properties. Start placing atoms from the center of the grid (row 3-4, col 3-4) and expand outward in all directions. Analyze the property gaps after each action and choose your next atom strategically.\"\n\nNUM_EPISODES = 200\n\ndataset = Dataset.from_dict({\n \"prompt\": [\n [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": USER_PROMPT},\n ]\n ] * NUM_EPISODES\n})\n\nprint(f\"Dataset created: {len(dataset)} episodes\")\nprint(f\"Sample prompt roles: {[m['role'] for m in dataset[0]['prompt']]}\")"
|
| 192 |
+
]
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"cell_type": "markdown",
|
| 196 |
+
"metadata": {},
|
| 197 |
+
"source": [
|
| 198 |
+
"## 7. Load Model with Unsloth\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"We use Unsloth for memory-efficient 4-bit QLoRA fine-tuning. Starting with `Qwen/Qwen3-0.6B` which fits on a free Colab T4 GPU. For better results with more compute, switch to `Qwen/Qwen3-1.7B` or `Qwen/Qwen2.5-3B-Instruct`."
|
| 201 |
+
]
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"cell_type": "code",
|
| 205 |
+
"execution_count": null,
|
| 206 |
+
"metadata": {},
|
| 207 |
+
"outputs": [],
|
| 208 |
+
"source": "import torch\nfrom unsloth import FastLanguageModel\n\nMODEL_NAME = \"Qwen/Qwen3-0.6B\" # Change to Qwen3-1.7B or Qwen2.5-3B-Instruct for better results\nMAX_SEQ_LENGTH = 4096\n\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n MODEL_NAME,\n max_seq_length=MAX_SEQ_LENGTH,\n load_in_4bit=True,\n dtype=None,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n model,\n r=16,\n lora_alpha=16,\n lora_dropout=0,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n \"gate_proj\", \"up_proj\", \"down_proj\"],\n use_gradient_checkpointing=\"unsloth\",\n)\n\n# --- Fix Unsloth + TRL compatibility for Qwen3 tool calling ---\n# Unsloth modifies the chat template, breaking TRL's string-comparison checks.\n# We load TRL's own Qwen3 training template (prefix-preserving, has {% generation %}\n# markers) and response schema directly.\nfrom pathlib import Path\nimport trl.chat_template_utils as _ctu\n\n# 1) Set the response schema so TRL can parse <tool_call> blocks\ntokenizer.response_schema = {\n \"x-regex\": r\"^(?:<think>\\n?(?:(?P<reasoning_content>.*?\\S.*?)\\n?|[\\s]*)</think>\\s*)?(?P<content>.*?)(?:\\n(?=<tool_call>))?(?=(?:<tool_call>|<\\|im_end\\|>|$))(?P<tool_calls>(?:<tool_call>.+?</tool_call>\\s*)+)?\\s*(?:<\\|im_end\\|>|$)\",\n \"type\": \"object\",\n \"properties\": {\n \"role\": {\"const\": \"assistant\"},\n \"content\": {\"type\": \"string\"},\n \"reasoning_content\": {\"type\": \"string\"},\n \"tool_calls\": {\n \"type\": \"array\",\n \"x-regex-iterator\": r\"<tool_call>\\s*(.+?)\\s*</tool_call>\",\n \"items\": {\n \"x-parser\": \"json\",\n \"x-parser-args\": {\"transform\": \"{type: 'function', function: @}\"},\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\"const\": \"function\"},\n \"function\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\"type\": \"string\"},\n \"arguments\": {\"type\": \"object\", \"additionalProperties\": {}},\n },\n },\n },\n },\n },\n },\n}\n\n# 2) Replace Unsloth's modified template with TRL's training-safe Qwen3 template\n_templates_dir = Path(_ctu.__file__).parent / \"chat_templates\"\ntokenizer.chat_template = (_templates_dir / \"qwen3_training.jinja\").read_text()\n\nprint(f\"Model loaded: {MODEL_NAME}\")\nprint(f\"Trainable params: {model.print_trainable_parameters()}\")\nprint(f\"Chat template: TRL qwen3_training.jinja (prefix-preserving)\")\nprint(f\"Response schema: Qwen3 tool-call parser\")"
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"cell_type": "markdown",
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"source": [
|
| 214 |
+
"## 8. GRPO Training\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"The `GRPOTrainer` with `environment_factory` handles the full multi-turn loop:\n",
|
| 217 |
+
"1. Generates completions from the LLM\n",
|
| 218 |
+
"2. Parses tool calls (`place_atom`, `remove_atom`, `replace_atom`)\n",
|
| 219 |
+
"3. Executes them against the MaterialForge environment\n",
|
| 220 |
+
"4. Feeds observations back to the model\n",
|
| 221 |
+
"5. Collects rewards and updates the policy via GRPO"
|
| 222 |
+
]
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"cell_type": "code",
|
| 226 |
+
"execution_count": null,
|
| 227 |
+
"metadata": {},
|
| 228 |
+
"outputs": [],
|
| 229 |
+
"source": [
|
| 230 |
+
"from trl import GRPOConfig, GRPOTrainer\n\nOUTPUT_DIR = \"materialforge-grpo-checkpoints\"\n\ntraining_args = GRPOConfig(\n output_dir=OUTPUT_DIR,\n\n # Generation\n num_generations=4,\n max_completion_length=2048,\n temperature=1.0,\n\n # Optimization\n learning_rate=5e-5,\n per_device_train_batch_size=1,\n gradient_accumulation_steps=4,\n num_train_epochs=1,\n\n # Logging\n logging_steps=1,\n log_completions=True,\n num_completions_to_print=1,\n\n # Tool calling\n chat_template_kwargs={\"enable_thinking\": False},\n\n # Saving\n save_steps=50,\n save_total_limit=2,\n)\n\ntrainer = GRPOTrainer(\n model=model,\n processing_class=tokenizer,\n reward_funcs=reward_func,\n train_dataset=dataset,\n args=training_args,\n environment_factory=MaterialForgeTRLEnv,\n)\n\nprint(\"Trainer initialized. Starting GRPO training...\")\nprint(f\" Episodes: {NUM_EPISODES}\")\nprint(f\" Generations per prompt: {training_args.num_generations}\")\nprint(f\" Max completion length: {training_args.max_completion_length}\")\nprint(f\" Temperature: {training_args.temperature}\")\nprint(f\" Learning rate: {training_args.learning_rate}\")\nprint(f\" Gradient accumulation: {training_args.gradient_accumulation_steps}\")\nprint(f\" Total steps: {NUM_EPISODES // (training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps)}\")"
|
| 231 |
+
]
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"cell_type": "code",
|
| 235 |
+
"execution_count": null,
|
| 236 |
+
"metadata": {},
|
| 237 |
+
"outputs": [],
|
| 238 |
+
"source": [
|
| 239 |
+
"train_result = trainer.train()\n",
|
| 240 |
+
"print(f\"\\nTraining complete!\")\n",
|
| 241 |
+
"print(f\" Total steps: {trainer.state.global_step}\")\n",
|
| 242 |
+
"print(f\" Final loss: {train_result.training_loss:.4f}\")"
|
| 243 |
+
]
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"cell_type": "markdown",
|
| 247 |
+
"metadata": {},
|
| 248 |
+
"source": [
|
| 249 |
+
"## 9. Save Trained Model"
|
| 250 |
+
]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"cell_type": "code",
|
| 254 |
+
"execution_count": null,
|
| 255 |
+
"metadata": {},
|
| 256 |
+
"outputs": [],
|
| 257 |
+
"source": [
|
| 258 |
+
"SAVE_DIR = \"materialforge-grpo-trained\"\n",
|
| 259 |
+
"trainer.save_model(SAVE_DIR)\n",
|
| 260 |
+
"tokenizer.save_pretrained(SAVE_DIR)\n",
|
| 261 |
+
"print(f\"Model saved to {SAVE_DIR}/\")"
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"cell_type": "markdown",
|
| 266 |
+
"metadata": {},
|
| 267 |
+
"source": [
|
| 268 |
+
"## 10. Training Reward Curves\n",
|
| 269 |
+
"\n",
|
| 270 |
+
"Extract and plot the reward history from training logs. These plots are a **required deliverable** \u2014 they must be saved as images and embedded in the README."
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"cell_type": "code",
|
| 275 |
+
"execution_count": null,
|
| 276 |
+
"metadata": {},
|
| 277 |
+
"outputs": [],
|
| 278 |
+
"source": [
|
| 279 |
+
"import matplotlib.pyplot as plt\n",
|
| 280 |
+
"import json\n",
|
| 281 |
+
"\n",
|
| 282 |
+
"os.makedirs(\"plots\", exist_ok=True)\n",
|
| 283 |
+
"\n",
|
| 284 |
+
"log_history = trainer.state.log_history\n",
|
| 285 |
+
"\n",
|
| 286 |
+
"steps = []\n",
|
| 287 |
+
"rewards = []\n",
|
| 288 |
+
"losses = []\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"for entry in log_history:\n",
|
| 291 |
+
" if \"reward\" in entry:\n",
|
| 292 |
+
" steps.append(entry.get(\"step\", len(steps)))\n",
|
| 293 |
+
" rewards.append(entry[\"reward\"])\n",
|
| 294 |
+
" if \"loss\" in entry:\n",
|
| 295 |
+
" if len(losses) < len(steps):\n",
|
| 296 |
+
" losses.append(entry[\"loss\"])\n",
|
| 297 |
+
"\n",
|
| 298 |
+
"# --- Plot 1: Reward over training ---\n",
|
| 299 |
+
"fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 300 |
+
"ax.plot(steps[:len(rewards)], rewards, color=\"#4285f4\", linewidth=2, label=\"GRPO Reward\")\n",
|
| 301 |
+
"if len(rewards) > 5:\n",
|
| 302 |
+
" window = max(3, len(rewards) // 10)\n",
|
| 303 |
+
" smoothed = [sum(rewards[max(0,i-window):i+1])/len(rewards[max(0,i-window):i+1]) for i in range(len(rewards))]\n",
|
| 304 |
+
" ax.plot(steps[:len(smoothed)], smoothed, color=\"#ea4335\", linewidth=2, linestyle=\"--\", label=f\"Smoothed (window={window})\")\n",
|
| 305 |
+
"ax.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 306 |
+
"ax.set_ylabel(\"Reward\", fontsize=12)\n",
|
| 307 |
+
"ax.set_title(\"MaterialForge GRPO Training \u2014 Reward Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 308 |
+
"ax.legend()\n",
|
| 309 |
+
"ax.grid(True, alpha=0.3)\n",
|
| 310 |
+
"fig.tight_layout()\n",
|
| 311 |
+
"fig.savefig(\"plots/reward_curve.png\", dpi=150)\n",
|
| 312 |
+
"plt.show()\n",
|
| 313 |
+
"print(\"Saved: plots/reward_curve.png\")\n",
|
| 314 |
+
"\n",
|
| 315 |
+
"# --- Plot 2: Loss over training ---\n",
|
| 316 |
+
"if losses:\n",
|
| 317 |
+
" fig2, ax2 = plt.subplots(1, 1, figsize=(10, 5))\n",
|
| 318 |
+
" ax2.plot(range(len(losses)), losses, color=\"#34a853\", linewidth=2)\n",
|
| 319 |
+
" ax2.set_xlabel(\"Training Step\", fontsize=12)\n",
|
| 320 |
+
" ax2.set_ylabel(\"Loss\", fontsize=12)\n",
|
| 321 |
+
" ax2.set_title(\"MaterialForge GRPO Training \u2014 Loss Curve\", fontsize=14, fontweight=\"bold\")\n",
|
| 322 |
+
" ax2.grid(True, alpha=0.3)\n",
|
| 323 |
+
" fig2.tight_layout()\n",
|
| 324 |
+
" fig2.savefig(\"plots/loss_curve.png\", dpi=150)\n",
|
| 325 |
+
" plt.show()\n",
|
| 326 |
+
" print(\"Saved: plots/loss_curve.png\")"
|
| 327 |
+
]
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"cell_type": "markdown",
|
| 331 |
+
"metadata": {},
|
| 332 |
+
"source": [
|
| 333 |
+
"## 11. Baseline Comparison\n",
|
| 334 |
+
"\n",
|
| 335 |
+
"Compare the trained agent against a random baseline to demonstrate improvement."
|
| 336 |
+
]
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"cell_type": "code",
|
| 340 |
+
"execution_count": null,
|
| 341 |
+
"metadata": {},
|
| 342 |
+
"outputs": [],
|
| 343 |
+
"source": [
|
| 344 |
+
"import random as rng\n",
|
| 345 |
+
"\n",
|
| 346 |
+
"def run_random_baseline(n_episodes=20, max_steps=30):\n",
|
| 347 |
+
" \"\"\"Run a random agent as baseline.\"\"\"\n",
|
| 348 |
+
" env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())\n",
|
| 349 |
+
" episode_rewards = []\n",
|
| 350 |
+
" episode_best = []\n",
|
| 351 |
+
"\n",
|
| 352 |
+
" for ep in range(n_episodes):\n",
|
| 353 |
+
" obs = env.reset(seed=ep * 7, difficulty=\"easy\")\n",
|
| 354 |
+
" rewards = []\n",
|
| 355 |
+
" for step in range(max_steps):\n",
|
| 356 |
+
" action_type = rng.choice([\"place\", \"place\", \"place\", \"replace\", \"remove\"])\n",
|
| 357 |
+
" row = rng.randint(0, 7)\n",
|
| 358 |
+
" col = rng.randint(0, 7)\n",
|
| 359 |
+
" atom = rng.choice([\"A\", \"B\", \"C\", \"P\"]) if action_type != \"remove\" else None\n",
|
| 360 |
+
" action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)\n",
|
| 361 |
+
" obs = env.step(action)\n",
|
| 362 |
+
" rewards.append(obs.reward if obs.reward else 0.0)\n",
|
| 363 |
+
" if obs.done:\n",
|
| 364 |
+
" break\n",
|
| 365 |
+
" episode_rewards.append(sum(rewards) / len(rewards) if rewards else 0.0)\n",
|
| 366 |
+
" episode_best.append(max(rewards) if rewards else 0.0)\n",
|
| 367 |
+
"\n",
|
| 368 |
+
" return episode_rewards, episode_best\n",
|
| 369 |
+
"\n",
|
| 370 |
+
"print(\"Running random baseline (20 episodes)...\")\n",
|
| 371 |
+
"random_avg_rewards, random_best_rewards = run_random_baseline(20)\n",
|
| 372 |
+
"print(f\"Random Agent \u2014 Mean reward: {sum(random_avg_rewards)/len(random_avg_rewards):.4f}\")\n",
|
| 373 |
+
"print(f\"Random Agent \u2014 Mean best: {sum(random_best_rewards)/len(random_best_rewards):.4f}\")"
|
| 374 |
+
]
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"cell_type": "code",
|
| 378 |
+
"execution_count": null,
|
| 379 |
+
"metadata": {},
|
| 380 |
+
"outputs": [],
|
| 381 |
+
"source": [
|
| 382 |
+
"# Comparison bar chart\n",
|
| 383 |
+
"trained_mean_reward = sum(rewards) / len(rewards) if rewards else 0.0\n",
|
| 384 |
+
"random_mean_reward = sum(random_avg_rewards) / len(random_avg_rewards)\n",
|
| 385 |
+
"\n",
|
| 386 |
+
"fig3, ax3 = plt.subplots(figsize=(8, 5))\n",
|
| 387 |
+
"agents = [\"Random\\nBaseline\", \"GRPO-Trained\\nAgent\"]\n",
|
| 388 |
+
"means = [random_mean_reward, trained_mean_reward]\n",
|
| 389 |
+
"colors = [\"#ea4335\", \"#4285f4\"]\n",
|
| 390 |
+
"bars = ax3.bar(agents, means, color=colors, width=0.5, edgecolor=\"white\", linewidth=1.5)\n",
|
| 391 |
+
"\n",
|
| 392 |
+
"for bar, val in zip(bars, means):\n",
|
| 393 |
+
" ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n",
|
| 394 |
+
" f\"{val:.4f}\", ha=\"center\", va=\"bottom\", fontweight=\"bold\", fontsize=13)\n",
|
| 395 |
+
"\n",
|
| 396 |
+
"ax3.set_ylabel(\"Mean Reward\", fontsize=12)\n",
|
| 397 |
+
"ax3.set_title(\"MaterialForge \u2014 Random Baseline vs GRPO-Trained Agent\", fontsize=14, fontweight=\"bold\")\n",
|
| 398 |
+
"ax3.set_ylim(0, max(means) * 1.3 + 0.05)\n",
|
| 399 |
+
"ax3.grid(axis=\"y\", alpha=0.3)\n",
|
| 400 |
+
"fig3.tight_layout()\n",
|
| 401 |
+
"fig3.savefig(\"plots/baseline_comparison.png\", dpi=150)\n",
|
| 402 |
+
"plt.show()\n",
|
| 403 |
+
"print(\"Saved: plots/baseline_comparison.png\")"
|
| 404 |
+
]
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"cell_type": "markdown",
|
| 408 |
+
"metadata": {},
|
| 409 |
+
"source": [
|
| 410 |
+
"## 12. Trained Agent Inference Demo\n",
|
| 411 |
+
"\n",
|
| 412 |
+
"Run the trained model on a fresh episode to see its crystal design strategy."
|
| 413 |
+
]
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"cell_type": "code",
|
| 417 |
+
"execution_count": null,
|
| 418 |
+
"metadata": {},
|
| 419 |
+
"outputs": [],
|
| 420 |
+
"source": [
|
| 421 |
+
"from transformers import AutoTokenizer, AutoModelForCausalLM\n",
|
| 422 |
+
"\n",
|
| 423 |
+
"print(\"Loading trained model for inference...\")\n",
|
| 424 |
+
"inf_model, inf_tokenizer = FastLanguageModel.from_pretrained(\n",
|
| 425 |
+
" SAVE_DIR,\n",
|
| 426 |
+
" max_seq_length=MAX_SEQ_LENGTH,\n",
|
| 427 |
+
" load_in_4bit=True,\n",
|
| 428 |
+
")\n",
|
| 429 |
+
"FastLanguageModel.for_inference(inf_model)\n",
|
| 430 |
+
"\n",
|
| 431 |
+
"demo_env = MaterialForgeTRLEnv()\n",
|
| 432 |
+
"obs_text = demo_env.reset()\n",
|
| 433 |
+
"\n",
|
| 434 |
+
"messages = [\n",
|
| 435 |
+
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 436 |
+
" {\"role\": \"user\", \"content\": USER_PROMPT},\n",
|
| 437 |
+
" {\"role\": \"assistant\", \"content\": f\"Let me analyze the initial state:\\n{obs_text}\\n\\nI'll start placing atoms strategically.\"},\n",
|
| 438 |
+
"]\n",
|
| 439 |
+
"\n",
|
| 440 |
+
"print(\"=\" * 60)\n",
|
| 441 |
+
"print(\"TRAINED AGENT DEMO\")\n",
|
| 442 |
+
"print(\"=\" * 60)\n",
|
| 443 |
+
"print(f\"\\nInitial observation:\\n{obs_text}\")\n",
|
| 444 |
+
"print(f\"\\nAgent designing crystal...\")\n",
|
| 445 |
+
"print(f\"(Full multi-turn inference requires TRL's tool-call loop.\")\n",
|
| 446 |
+
"print(f\"See the training logs above for actual agent behavior.)\")"
|
| 447 |
+
]
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"cell_type": "markdown",
|
| 451 |
+
"metadata": {},
|
| 452 |
+
"source": [
|
| 453 |
+
"## 13. Summary\n",
|
| 454 |
+
"\n",
|
| 455 |
+
"| Metric | Random Baseline | GRPO-Trained Agent |\n",
|
| 456 |
+
"|--------|----------------|--------------------|\n",
|
| 457 |
+
"| Mean Reward | *see above* | *see above* |\n",
|
| 458 |
+
"| Phase Achieved | Amorphous | *see training logs* |\n",
|
| 459 |
+
"\n",
|
| 460 |
+
"### Deliverables produced by this notebook:\n",
|
| 461 |
+
"- `plots/reward_curve.png` \u2014 Reward over training steps\n",
|
| 462 |
+
"- `plots/loss_curve.png` \u2014 Training loss curve\n",
|
| 463 |
+
"- `plots/baseline_comparison.png` \u2014 Random vs trained comparison\n",
|
| 464 |
+
"- `materialforge-grpo-trained/` \u2014 Saved model checkpoint\n",
|
| 465 |
+
"\n",
|
| 466 |
+
"### Links\n",
|
| 467 |
+
"- **Live Environment**: [ArshPathan/material_forge_env](https://huggingface.co/spaces/ArshPathan/material_forge_env)\n",
|
| 468 |
+
"- **Repository**: [MaterialForge GitHub](https://github.com/Arsh-Pathan/MaterialForge)"
|
| 469 |
+
]
|
| 470 |
+
}
|
| 471 |
+
],
|
| 472 |
+
"metadata": {
|
| 473 |
+
"accelerator": "GPU",
|
| 474 |
+
"colab": {
|
| 475 |
+
"gpuType": "T4",
|
| 476 |
+
"provenance": [],
|
| 477 |
+
"name": "MaterialForge_GRPO_Training.ipynb"
|
| 478 |
+
},
|
| 479 |
+
"kernelspec": {
|
| 480 |
+
"display_name": "Python 3",
|
| 481 |
+
"name": "python3"
|
| 482 |
+
},
|
| 483 |
+
"language_info": {
|
| 484 |
+
"name": "python",
|
| 485 |
+
"version": "3.11.0"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"nbformat": 4,
|
| 489 |
+
"nbformat_minor": 0
|
| 490 |
+
}
|
training/train_env_wrapper.py
CHANGED
|
@@ -1,198 +1,143 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MaterialForge TRL Environment Wrapper
|
| 3 |
-
======================================
|
| 4 |
-
Wraps the MaterialForge environment for use with TRL's GRPOTrainer
|
| 5 |
-
via the environment_factory pattern. Exposes crystal lattice operations
|
| 6 |
-
as named tool methods that the LLM discovers and calls during training.
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
import
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
import
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
self.
|
| 59 |
-
self._obs =
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
self.
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
self.
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
self.
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
row
|
| 90 |
-
col
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
if
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if action_type == "remove" and cell == ".":
|
| 146 |
-
self._invalid_actions += 1
|
| 147 |
-
return f"INVALID ACTION: Cannot remove from ({row},{col}) because it is already empty."
|
| 148 |
-
|
| 149 |
-
if action_type == "replace" and (cell == "." or cell == atom):
|
| 150 |
-
self._invalid_actions += 1
|
| 151 |
-
return f"INVALID ACTION: replace_atom at ({row},{col}) is redundant or invalid. Use place_atom if empty."
|
| 152 |
-
|
| 153 |
-
# Execute valid action
|
| 154 |
-
action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)
|
| 155 |
-
obs = self.env.step(action)
|
| 156 |
-
self._obs = obs
|
| 157 |
-
|
| 158 |
-
# Track the best reward seen in this episode for the GRPO signal
|
| 159 |
-
step_reward = obs.reward if obs.reward is not None else 0.0
|
| 160 |
-
self._best_reward = max(self._best_reward, step_reward)
|
| 161 |
-
|
| 162 |
-
# Signal to GRPOTrainer (includes a penalty for invalid attempts)
|
| 163 |
-
penalty = 0.05 * self._invalid_actions
|
| 164 |
-
self.reward = max(self._best_reward - penalty, 0.0)
|
| 165 |
-
|
| 166 |
-
self.done = obs.done
|
| 167 |
-
return self._format_observation(obs)
|
| 168 |
-
|
| 169 |
-
def _format_observation(self, obs: MaterialForgeObservation) -> str:
|
| 170 |
-
grid_lines = []
|
| 171 |
-
for r, row in enumerate(obs.grid):
|
| 172 |
-
grid_lines.append(f" {r}: {' '.join(cell if cell != '.' else '.' for cell in row)}")
|
| 173 |
-
grid_str = "\n".join(grid_lines)
|
| 174 |
-
|
| 175 |
-
gaps = {}
|
| 176 |
-
for prop in PROPERTY_NAMES:
|
| 177 |
-
t = obs.target.get(prop, 0.0)
|
| 178 |
-
c = obs.current_properties.get(prop, 0.0)
|
| 179 |
-
gaps[prop] = round(t - c, 1)
|
| 180 |
-
|
| 181 |
-
props_str = ", ".join(
|
| 182 |
-
f"{p}={obs.current_properties.get(p, 0):.1f}/{obs.target.get(p, 0):.1f} (gap:{gaps[p]:+.1f})"
|
| 183 |
-
for p in PROPERTY_NAMES
|
| 184 |
-
)
|
| 185 |
-
|
| 186 |
-
sb = obs.score_breakdown or {}
|
| 187 |
-
stability = sb.get("structural_stability", 0.0)
|
| 188 |
-
order = sb.get("lattice_order_index", 0.0)
|
| 189 |
-
|
| 190 |
-
return (
|
| 191 |
-
f"Step {obs.step_number}/{obs.max_steps} | "
|
| 192 |
-
f"Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | "
|
| 193 |
-
f"Phase: {obs.phase} | "
|
| 194 |
-
f"Reward: {obs.reward:.4f}\n"
|
| 195 |
-
f"Properties: {props_str}\n"
|
| 196 |
-
f"Stability: {stability:.3f} | Order: {order:.3f}\n"
|
| 197 |
-
f"Grid (row: cols 0-7):\n{grid_str}"
|
| 198 |
-
)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MaterialForge TRL Environment Wrapper
|
| 3 |
+
======================================
|
| 4 |
+
Wraps the MaterialForge environment for use with TRL's GRPOTrainer
|
| 5 |
+
via the environment_factory pattern. Exposes crystal lattice operations
|
| 6 |
+
as named tool methods that the LLM discovers and calls during training.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import random
|
| 10 |
+
from typing import Optional
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 15 |
+
|
| 16 |
+
from environment.config import GRID_SIZE, PROPERTY_NAMES, ATOM_TYPES
|
| 17 |
+
from environment.rubrics import HeuristicRewardRubric
|
| 18 |
+
from models import MaterialForgeAction, MaterialForgeObservation
|
| 19 |
+
from server.material_forge_env_environment import MaterialForgeEnvironment
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# --- Curriculum Learning State ---
|
| 23 |
+
TOTAL_EPISODES_STARTED = 0
|
| 24 |
+
|
| 25 |
+
def get_curriculum_difficulty() -> str:
|
| 26 |
+
"""Selects difficulty based on global episode progress (scaled for 100 episodes)."""
|
| 27 |
+
global TOTAL_EPISODES_STARTED
|
| 28 |
+
if TOTAL_EPISODES_STARTED < 25:
|
| 29 |
+
difficulty = "easy"
|
| 30 |
+
elif TOTAL_EPISODES_STARTED < 75:
|
| 31 |
+
difficulty = "medium"
|
| 32 |
+
else:
|
| 33 |
+
difficulty = "hard"
|
| 34 |
+
|
| 35 |
+
TOTAL_EPISODES_STARTED += 1
|
| 36 |
+
return difficulty
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class MaterialForgeTRLEnv:
|
| 40 |
+
"""TRL-compatible wrapper for MaterialForge crystal design."""
|
| 41 |
+
|
| 42 |
+
def __init__(self):
|
| 43 |
+
self.env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())
|
| 44 |
+
self.reward = 0.0
|
| 45 |
+
self.done = False
|
| 46 |
+
self._obs = None
|
| 47 |
+
|
| 48 |
+
# Performance Tracking
|
| 49 |
+
self._best_reward = 0.0
|
| 50 |
+
self._invalid_actions = 0
|
| 51 |
+
self._total_actions = 0
|
| 52 |
+
|
| 53 |
+
def reset(self, **kwargs) -> str:
|
| 54 |
+
"""Resets the environment with a curriculum-based difficulty."""
|
| 55 |
+
seed = random.randint(0, 99999)
|
| 56 |
+
difficulty = get_curriculum_difficulty()
|
| 57 |
+
|
| 58 |
+
obs = self.env.reset(seed=seed, difficulty=difficulty)
|
| 59 |
+
self._obs = obs
|
| 60 |
+
self.reward = 0.0
|
| 61 |
+
self.done = False
|
| 62 |
+
|
| 63 |
+
# Reset counters
|
| 64 |
+
self._best_reward = 0.0
|
| 65 |
+
self._invalid_actions = 0
|
| 66 |
+
self._total_actions = 0
|
| 67 |
+
|
| 68 |
+
return self._format_observation(obs)
|
| 69 |
+
|
| 70 |
+
def place_atom(self, row: int, col: int, atom: str) -> str:
|
| 71 |
+
"""Place an atom on an empty cell of the crystal lattice."""
|
| 72 |
+
return self._do_step("place", row, col, atom)
|
| 73 |
+
|
| 74 |
+
def remove_atom(self, row: int, col: int) -> str:
|
| 75 |
+
"""Remove an atom from the crystal lattice."""
|
| 76 |
+
return self._do_step("remove", row, col, None)
|
| 77 |
+
|
| 78 |
+
def replace_atom(self, row: int, col: int, atom: str) -> str:
|
| 79 |
+
"""Replace an existing atom with a different species."""
|
| 80 |
+
return self._do_step("replace", row, col, atom)
|
| 81 |
+
|
| 82 |
+
def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:
|
| 83 |
+
if self.done:
|
| 84 |
+
return "ERROR: Episode is over. Stop calling tools."
|
| 85 |
+
|
| 86 |
+
self._total_actions += 1
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
row = max(0, min(int(row), GRID_SIZE - 1))
|
| 90 |
+
col = max(0, min(int(col), GRID_SIZE - 1))
|
| 91 |
+
except (ValueError, TypeError):
|
| 92 |
+
self._invalid_actions += 1
|
| 93 |
+
return "ERROR: Coordinates must be integers 0-7."
|
| 94 |
+
|
| 95 |
+
if atom is not None:
|
| 96 |
+
atom = str(atom).upper().strip()
|
| 97 |
+
if atom not in ATOM_TYPES:
|
| 98 |
+
atom = "A"
|
| 99 |
+
|
| 100 |
+
# --- Invalid Action Pre-check ---
|
| 101 |
+
grid = self._obs.grid if self._obs else None
|
| 102 |
+
if grid:
|
| 103 |
+
cell = grid[row][col]
|
| 104 |
+
if action_type == "place" and cell != ".":
|
| 105 |
+
self._invalid_actions += 1
|
| 106 |
+
return f"INVALID ACTION: ({row},{col}) is already occupied by '{cell}'. Use replace_atom."
|
| 107 |
+
|
| 108 |
+
if action_type == "remove" and cell == ".":
|
| 109 |
+
self._invalid_actions += 1
|
| 110 |
+
return f"INVALID ACTION: Cannot remove from empty cell ({row},{col})."
|
| 111 |
+
|
| 112 |
+
if action_type == "replace" and (cell == "." or cell == atom):
|
| 113 |
+
self._invalid_actions += 1
|
| 114 |
+
return f"INVALID ACTION: replace_atom at ({row},{col}) is redundant or invalid."
|
| 115 |
+
|
| 116 |
+
action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)
|
| 117 |
+
obs = self.env.step(action)
|
| 118 |
+
self._obs = obs
|
| 119 |
+
|
| 120 |
+
# Track best reward and apply penalty for invalid attempts
|
| 121 |
+
step_reward = obs.reward if obs.reward is not None else 0.0
|
| 122 |
+
self._best_reward = max(self._best_reward, step_reward)
|
| 123 |
+
|
| 124 |
+
penalty = 0.05 * self._invalid_actions
|
| 125 |
+
self.reward = max(self._best_reward - penalty, 0.0)
|
| 126 |
+
|
| 127 |
+
self.done = obs.done
|
| 128 |
+
return self._format_observation(obs)
|
| 129 |
+
|
| 130 |
+
def _format_observation(self, obs: MaterialForgeObservation) -> str:
|
| 131 |
+
grid_lines = []
|
| 132 |
+
for r, row in enumerate(obs.grid):
|
| 133 |
+
grid_lines.append(f" {r}: {' '.join(cell if cell != '.' else '.' for cell in row)}")
|
| 134 |
+
|
| 135 |
+
gaps = ", ".join([f"{p}={obs.current_properties.get(p,0):.1f}/{obs.target.get(p,0):.1f}" for p in PROPERTY_NAMES])
|
| 136 |
+
sb = obs.score_breakdown or {}
|
| 137 |
+
|
| 138 |
+
return (
|
| 139 |
+
f"Step {obs.step_number}/{obs.max_steps} | Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | Phase: {obs.phase}\n"
|
| 140 |
+
f"Reward: {obs.reward:.4f} | Properties: {gaps}\n"
|
| 141 |
+
f"Stability: {sb.get('structural_stability',0):.3f} | Order: {sb.get('lattice_order_index',0):.3f}\n"
|
| 142 |
+
f"Grid:\n{chr(10).join(grid_lines)}"
|
| 143 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|