ArshPathan commited on
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 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
- │ └── 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
 
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.45,
24
- "thermal_resistance": 0.35,
25
- "elasticity": 0.10,
26
  },
27
  },
28
  "B": {
29
  "name": "conductor",
30
  "cost": 6,
31
  "contributions": {
32
- "hardness": 0.20,
33
- "conductivity": 1.00,
34
- "thermal_resistance": 0.15,
35
- "elasticity": 0.30,
36
  },
37
  },
38
  "C": {
39
  "name": "ceramic",
40
  "cost": 4,
41
  "contributions": {
42
- "hardness": 0.65,
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.10,
53
- "conductivity": 0.10,
54
- "thermal_resistance": 0.20,
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 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
  ],
@@ -473,8 +109,7 @@
473
  "accelerator": "GPU",
474
  "colab": {
475
  "gpuType": "T4",
476
- "provenance": [],
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
- The OpenEnv environment itself keeps its standard step() API unchanged.
9
- This wrapper translates named tool calls into MaterialForgeAction objects.
10
- """
11
-
12
- import random
13
- from typing import Optional
14
-
15
- import sys
16
- import os
17
-
18
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
19
-
20
- from environment.config import GRID_SIZE, PROPERTY_NAMES, ATOM_TYPES
21
- from environment.lattice import Lattice
22
- from environment.physics import classify_phase, compute_structural_stability, compute_lattice_order, estimate_properties
23
- from environment.rubrics import HeuristicRewardRubric
24
- from models import MaterialForgeAction, MaterialForgeObservation
25
- from server.material_forge_env_environment import MaterialForgeEnvironment
26
-
27
-
28
- # --- Curriculum Learning State ---
29
- # Tracks how many episodes have been started across all trainer instances
30
- # to slowly ramp up difficulty from Easy -> Medium -> Hard.
31
- TOTAL_EPISODES_STARTED = 0
32
-
33
- def get_curriculum_difficulty() -> str:
34
- """Selects difficulty based on global episode progress."""
35
- global TOTAL_EPISODES_STARTED
36
- if TOTAL_EPISODES_STARTED < 40:
37
- difficulty = "easy"
38
- elif TOTAL_EPISODES_STARTED < 120:
39
- difficulty = "medium"
40
- else:
41
- difficulty = "hard"
42
-
43
- TOTAL_EPISODES_STARTED += 1
44
- return difficulty
45
-
46
-
47
- class MaterialForgeTRLEnv:
48
- """TRL-compatible wrapper for MaterialForge crystal design environment.
49
-
50
- Exposes place_atom, remove_atom, replace_atom as tool methods that
51
- GRPOTrainer automatically discovers and presents to the LLM as
52
- callable functions during multi-turn rollouts.
53
- """
54
-
55
- def __init__(self):
56
- self.env = MaterialForgeEnvironment(rubric=HeuristicRewardRubric())
57
- self.reward = 0.0
58
- self.done = False
59
- self._obs = None
60
-
61
- # Performance Tracking
62
- self._best_reward = 0.0
63
- self._invalid_actions = 0
64
- self._total_actions = 0
65
-
66
- def reset(self, **kwargs) -> str:
67
- """Resets the environment with a curriculum-based difficulty."""
68
- seed = random.randint(0, 99999)
69
-
70
- # Apply Curriculum Learning
71
- difficulty = get_curriculum_difficulty()
72
-
73
- obs = self.env.reset(seed=seed, difficulty=difficulty)
74
- self._obs = obs
75
- self.reward = 0.0
76
- self.done = False
77
-
78
- # Reset counters
79
- self._best_reward = 0.0
80
- self._invalid_actions = 0
81
- self._total_actions = 0
82
-
83
- return self._format_observation(obs)
84
-
85
- def place_atom(self, row: int, col: int, atom: str) -> str:
86
- """Place an atom on an empty cell of the crystal lattice.
87
-
88
- Args:
89
- row: Row index on the 8x8 grid (0-7)
90
- col: Column index on the 8x8 grid (0-7)
91
- atom: Atom species to place - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)
92
-
93
- Returns:
94
- Updated lattice state or an error message if the placement is invalid.
95
- """
96
- return self._do_step("place", row, col, atom)
97
-
98
- def remove_atom(self, row: int, col: int) -> str:
99
- """Remove an atom from the crystal lattice to free up budget.
100
-
101
- Args:
102
- row: Row index on the 8x8 grid (0-7)
103
- col: Column index on the 8x8 grid (0-7)
104
- """
105
- return self._do_step("remove", row, col, None)
106
-
107
- def replace_atom(self, row: int, col: int, atom: str) -> str:
108
- """Replace an existing atom with a different species.
109
-
110
- Args:
111
- row: Row index on the 8x8 grid (0-7)
112
- col: Column index on the 8x8 grid (0-7)
113
- atom: New atom species - 'A', 'B', 'C', or 'P'
114
- """
115
- return self._do_step("replace", row, col, atom)
116
-
117
- def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:
118
- if self.done:
119
- return "ERROR: Episode is over. Stop calling tools and provide your final answer."
120
-
121
- self._total_actions += 1
122
-
123
- # Clamp inputs to valid range
124
- try:
125
- row = max(0, min(int(row), GRID_SIZE - 1))
126
- col = max(0, min(int(col), GRID_SIZE - 1))
127
- except (ValueError, TypeError):
128
- self._invalid_actions += 1
129
- return "ERROR: Coordinates must be integers between 0 and 7."
130
-
131
- if atom is not None:
132
- atom = str(atom).upper().strip()
133
- if atom not in ATOM_TYPES:
134
- atom = "A"
135
-
136
- # --- Invalid Action Pre-check ---
137
- # Prevents the model from making no-op moves that waste steps and budget.
138
- grid = self._obs.grid if self._obs else None
139
- if grid:
140
- cell = grid[row][col]
141
- if action_type == "place" and cell != ".":
142
- self._invalid_actions += 1
143
- return f"INVALID ACTION: ({row},{col}) is already occupied by '{cell}'. Use replace_atom or pick an empty cell ('.')."
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
+ )