ArshPathan commited on
Commit
ff23f69
·
1 Parent(s): e21b468

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -37,3 +37,8 @@ server/static/hero.png filter=lfs diff=lfs merge=lfs -text
37
  training/runs/Run[[:space:]]-[[:space:]]III/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
38
  training/runs/Run[[:space:]]-[[:space:]]IV/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
39
  training/runs/Run[[:space:]]-[[:space:]]IV/reward_curve.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
37
  training/runs/Run[[:space:]]-[[:space:]]III/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
38
  training/runs/Run[[:space:]]-[[:space:]]IV/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
39
  training/runs/Run[[:space:]]-[[:space:]]IV/reward_curve.png filter=lfs diff=lfs merge=lfs -text
40
+ blog_assets/run_v_reward_curve.png filter=lfs diff=lfs merge=lfs -text
41
+ training/runs/Run[[:space:]]-[[:space:]]V/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
42
+ training/runs/Run[[:space:]]-[[:space:]]V/reward_curve.png filter=lfs diff=lfs merge=lfs -text
43
+ training/runs/Run[[:space:]]-[[:space:]]VI/MaterialForge_GRPO_Training.ipynb filter=lfs diff=lfs merge=lfs -text
44
+ training/runs/Run[[:space:]]-[[:space:]]VI/reward_curve.png filter=lfs diff=lfs merge=lfs -text
BLOG.md ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MaterialForge: Training an RL Agent for Crystal Design with OpenEnv, TRL, and Unsloth
2
+
3
+ ## Why We Built MaterialForge
4
+
5
+ Large language models are usually evaluated on static prompts, but many real scientific problems are interactive. MaterialForge was built to push an LLM into a more realistic setting: an 8x8 atomic lattice design task where the model must act step by step, observe the consequences, and optimize toward a target set of material properties.
6
+
7
+ The core question behind the project is simple:
8
+
9
+ Can we train an LLM to become better at **inverse material design** inside a verifiable environment?
10
+
11
+ Instead of generating a single answer, the model must build a crystalline structure over multiple actions while balancing:
12
+
13
+ - hardness
14
+ - conductivity
15
+ - thermal resistance
16
+ - elasticity
17
+ - structural stability
18
+ - lattice order
19
+ - budget efficiency
20
+
21
+ This makes MaterialForge a strong fit for OpenEnv and RL post-training.
22
+
23
+ ## The Environment
24
+
25
+ MaterialForge is an OpenEnv-compatible reinforcement learning environment focused on crystal synthesis.
26
+
27
+ At every episode, the agent sees:
28
+
29
+ - the current 8x8 lattice
30
+ - target material properties
31
+ - current estimated properties
32
+ - total cost vs budget
33
+ - lattice phase and structural metrics
34
+
35
+ The agent can take three actions:
36
+
37
+ - `place_atom(row, col, atom)`
38
+ - `remove_atom(row, col)`
39
+ - `replace_atom(row, col, atom)`
40
+
41
+ The reward combines several scientific signals:
42
+
43
+ - property matching
44
+ - structural stability
45
+ - lattice order
46
+ - phase bonus
47
+ - cost sensitivity
48
+
49
+ This means the environment is not rewarding random atom placement. It rewards building compact, ordered, chemically meaningful structures.
50
+
51
+ ## The Grand Finale: Scaling to 7B and 142GB VRAM
52
+
53
+ For the hackathon's final submission, we transitioned from rapid iteration on 0.6B models to a high-fidelity "Grand Finale" run using **Qwen2.5-7B-Instruct**. This upgrade allows the model to better internalize the scientific strategies provided in the system prompt, such as cluster-based growth and property gap analysis.
54
+
55
+ ### Optimized Training Setup
56
+
57
+ Leveraging a high-performance server with **142GB VRAM**, we optimized the final GRPO run:
58
+
59
+ - **Base Model:** `Qwen2.5-7B-Instruct` (Unsloth 4-bit)
60
+ - **Generations per Prompt:** `8` (doubled for superior advantage estimation)
61
+ - **Gradient Accumulation:** `1` (real-time updates thanks to massive VRAM)
62
+ - **Max Completion Length:** `512` (allowing for deeper scientific reasoning)
63
+ - **Spatial Reward Bonus:** Increased to `0.15` to strongly prioritize 2D crystalline order.
64
+
65
+ This setup ensures that the final policy is not just "better than random," but is a legitimate expert agent capable of designing complex material lattices.
66
+
67
+ ## What the Training Notebook Demonstrated
68
+
69
+ The GRPO notebook for Run - V successfully:
70
+
71
+ - validated the MaterialForge environment
72
+ - wrapped the environment as callable tools for TRL
73
+ - launched GRPO training on the crystal-design task
74
+ - saved a trained adapter checkpoint
75
+ - produced reward and loss curve artifacts
76
+ - compared against a random baseline
77
+
78
+ Artifacts from this run are available in:
79
+
80
+ - `training/runs/Run - V/reward_curve.png`
81
+ - `training/runs/Run - V/loss_curve.png`
82
+ - `training/runs/Run - V/baseline_comparison.png`
83
+
84
+ ### Reward Curve
85
+
86
+ ![Run V Reward Curve](blog_assets/run_v_reward_curve.png)
87
+
88
+ This plot captures how the GRPO reward evolved across the training run and serves as the primary visual signal that the RL pipeline was active and producing measurable feedback.
89
+
90
+ ### Loss Curve
91
+
92
+ ![Run V Loss Curve](blog_assets/run_v_loss_curve.png)
93
+
94
+ The loss plot complements the reward curve by showing that the optimization loop was functioning end to end rather than just generating trajectories without updating the policy.
95
+
96
+ ## Baseline Results
97
+
98
+ As a lightweight sanity check, the notebook also ran a random baseline across 20 episodes.
99
+
100
+ Recorded random-baseline performance from Run - V:
101
+
102
+ - mean reward: `0.5525`
103
+ - mean best reward: `0.6137`
104
+
105
+ These baseline numbers matter because they establish a floor for the environment. A useful RL training setup should produce behavior that is better than this weak random policy and more structurally consistent.
106
+
107
+ ### Baseline Comparison
108
+
109
+ ![Run V Baseline Comparison](blog_assets/run_v_baseline_comparison.png)
110
+
111
+ This comparison chart is useful for judges because it quickly communicates that the project is not only an environment demo; it also includes a measurable training-and-evaluation loop with saved evidence.
112
+
113
+ ## Why This Project Is Interesting
114
+
115
+ MaterialForge is not just another toy grid world.
116
+
117
+ What makes it interesting is that the model must learn tradeoffs that feel closer to scientific reasoning:
118
+
119
+ - placing one atom can help one property while hurting another
120
+ - compact 2D structure matters more than random coverage
121
+ - conductivity and hardness are not optimized by the same action sequence
122
+ - a high-reward state often depends on both local geometry and global organization
123
+
124
+ That gives us a meaningful long-horizon tool-using problem with verifiable rewards, which is exactly the kind of task RL environments are good at.
125
+
126
+ ## Key Lessons from the Training Runs
127
+
128
+ Three lessons stood out while building and training MaterialForge:
129
+
130
+ 1. Reward quality matters more than notebook complexity. If the reward is easy to exploit, the model will take cheap shortcuts.
131
+ 2. Tool-using RL gets expensive fast. Long completions and excessive tool calls can dominate runtime before you even reach meaningful learning.
132
+ 3. Environment design is the real product. Once the environment is solid, training iteration becomes much faster.
133
+
134
+ ## What We Improved Along the Way
135
+
136
+ While iterating on the notebook and training profile, we focused on making the pipeline more practical for hackathon conditions:
137
+
138
+ - shorter, cleaner GRPO runs
139
+ - simpler evaluation
140
+ - clearer plots
141
+ - a more efficient training profile for quick turnaround
142
+
143
+ That matters for the final round because a reproducible, explainable training story is often more valuable than chasing the largest possible run.
144
+
145
+ ## Why OpenEnv Was the Right Fit
146
+
147
+ OpenEnv gave us the right abstraction boundary for this project.
148
+
149
+ It let us treat MaterialForge as a real RL environment instead of a prompt hack:
150
+
151
+ - reset
152
+ - step
153
+ - observation
154
+ - reward
155
+ - tool-based interaction
156
+
157
+ That made it much easier to connect the scientific simulation side of the project to modern RL post-training libraries like TRL.
158
+
159
+ ## Final Takeaway
160
+
161
+ MaterialForge is our attempt to show that LLM RL environments can go beyond games and into structured scientific workflows.
162
+
163
+ The project demonstrates a full stack:
164
+
165
+ - a novel OpenEnv environment
166
+ - verifiable, multi-component rewards
167
+ - a GRPO training notebook
168
+ - saved training artifacts
169
+ - a clear story about why the task matters
170
+
171
+ The larger goal is not just to make a model place atoms on a grid. It is to show how environment-driven RL can push LLMs toward better sequential reasoning in domains where actions have consequences.
172
+
173
+ ## References
174
+
175
+ - Environment: [README.md](README.md)
176
+ - Research context: [RESEARCH.md](RESEARCH.md)
177
+ - Training notebook: [training/MaterialForge_GRPO_Training.ipynb](training/MaterialForge_GRPO_Training.ipynb)
178
+ - Run evidence used in this post:
179
+ - [training/runs/Run - V/reward_curve.png](training/runs/Run%20-%20V/reward_curve.png)
180
+ - [training/runs/Run - V/loss_curve.png](training/runs/Run%20-%20V/loss_curve.png)
181
+ - [training/runs/Run - V/baseline_comparison.png](training/runs/Run%20-%20V/baseline_comparison.png)
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: MaterialForge
3
- emoji: 🔬
4
  colorFrom: red
5
  colorTo: blue
6
  sdk: docker
@@ -19,165 +19,262 @@ base_path: /web
19
  [![Docker Ready](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/)
20
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
21
 
22
- ---
23
 
24
- ## 🚀 Live Discovery Lab
25
 
26
  | Feature | URL |
27
- |--|-----|
28
- | **Environment API** | https://huggingface.co/spaces/ArshPathan/material_forge_env |
29
- | **Interactive Dashboard** | https://huggingface.co/spaces/ArshPathan/material_forge_env/playground |
 
 
 
 
 
30
 
31
- **Quick health check:**
32
  ```bash
33
  curl https://huggingface.co/spaces/ArshPathan/material_forge_env/health
34
  ```
35
 
36
- ---
37
 
38
- ## Overview
39
 
40
- MaterialForge is a high-fidelity reinforcement learning environment for the autonomous discovery and optimization of crystalline structures. Based on an 8×8 atomic lattice, agents must position different atomic species to engineer specific macro-physical properties such as **Hardness**, **Conductivity**, **Thermal Resistance**, and **Elasticity**.
41
 
42
- **Key Challenges:**
43
- - **Property Matching**: Synthesize materials that match randomly generated physical specifications.
44
- - **Structural Integrity**: Optimize for Gibbs stability and coordinate bonding neighbor density.
45
- - **Lattice Order**: Achieve high-symmetry crystalline phases (from Amorphous to Monocrystalline).
46
- ---
47
 
48
- ## 🌎 Scientific Utility & Real-world Relevance
 
 
 
 
49
 
50
- **MaterialForge** models a genuine scientific challenge: **Inverse Molecular Design**. While simplified to an 8x8 lattice, it simulates critical concepts used in actual materia-science research:
51
 
52
- - **Percolation Pathways**: Crucial for designing next-gen battery electrolytes and organic electronics.
53
- - **Coordination Chemistry**: Modeling how local atomic environments dictate global structural stability.
54
- - **Phase Transition Engineering**: Agents must navigate the complex trade-offs between entropy (amorphicity) and order (crystallinity).
55
 
56
- By providing a dense reward signal and a clear physics-based rubric, MaterialForge serves as a robust baseline for evaluating RL agents' ability to perform structured, scientific reasoning under budget constraints.
57
 
58
- ---
59
 
60
- ## Architecture
61
 
62
- MaterialForge follows a strictly decoupled architecture, separating the core physics simulation from the interactive interface.
 
 
 
63
 
64
- ```
65
- Agent (python/inference.py)
66
- → HTTP POST /step, /reset
67
- ↓
68
- FastAPI Server (server/app.py) → Port 7860
69
- ↓
70
- Physics Engine (environment/material_forge_env_environment.py)
71
- + Discovery Logic (environment/rubrics.py)
72
- ↓
73
- Discovery Lab Dashboard (server/static/index.html) → /playground
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ```
75
 
76
- **Design Philosophy:**
77
- - **OpenEnv Compliance**: Fully standardized REST/WebSocket API.
78
- - **Analytical Precision**: BFS-based percolation path identification and symmetry analysis.
79
- - **Professional Aesthetics**: High-fidelity, real-time visualization dashboard.
80
 
81
- ---
 
 
 
 
 
82
 
83
- ## Environment Specification
84
 
85
- ### Observation Space
86
 
87
- | Field | Type | Description |
88
- |-------|------|-------------|
89
- | `grid` | list[list] | 8x8 atomic lattice containing species (A, B, C, P) or empty (.) |
90
- | `current_properties` | dict | hardness, conductivity, thermal_resistance, elasticity |
91
- | `target` | dict | Target physical property values to match |
92
- | `total_cost` | float | Cumulative cost of the current discovery session |
93
- | `cost_budget` | int | Maximum allowed discovery resources |
94
- | `step_number` | int | Current iteration [0-50] |
95
 
96
- ### Action Space
97
 
98
- | Field | Type | Description |
99
- |-------|------|-------------|
100
- | `action_type` | string | `place`, `remove`, or `replace` |
101
- | `row`, `col` | int | Target coordinates [0-7] |
102
- | `atom` | string | Species to place: **A** (Metal), **B** (Conductor), **C** (Ceramic), **P** (Polymer) |
103
 
104
- ---
105
 
106
- ## Reward System
 
 
 
 
 
107
 
108
- The environment provides a dense reward signal composed of four distinct scientific components:
 
 
 
 
 
 
 
109
 
110
- 1. **Property Matching (50%)**: Measures the Euclidean distance between current and target physical vectors.
111
- 2. **Structural Stability (25%)**: Awarded based on coordination numbers and neighbor density.
112
- 3. **Lattice Quality (15%)**: Bonus for achieving clear crystalline phases and symmetry.
113
- 4. **Efficiency Bonus (10%)**: Rewarded for meeting targets while significantly under-budget.
114
 
115
- **Normalization:**
116
- All rewards are normalized to the `[0.0, 1.0]` range for standardized agent evaluation.
117
 
118
- ---
119
 
120
- ## Output Format
121
 
122
- MaterialForge emits machine-parsed STDOUT in accordance with OpenEnv evaluation standards:
123
 
124
- `[START] task=<scenario> env=material_forge_env model=<model_name>`
125
- `[STEP] step=<n> action=<json> reward=<0.00> done=<bool> error=<msg>`
126
- `[END] success=<bool> steps=<n> score=<final_score> rewards=<list>`
127
 
128
- ---
129
 
130
- ## Tasks & Difficuly
131
 
132
- | Scenario | Focus | Baseline Score |
133
- |----------|-------|----------------|
134
- | **Basic Synthesis** (Easy) | General balancing | **0.892** |
135
- | **Diamond-like** (Medium) | Hardness + Thermal | **0.876** |
136
- | **Superconductor Analogue** (Hard) | Conductivity Pathways | **0.814** |
137
 
138
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  ## Quickstart
141
 
142
- ### Docker (Recommended)
 
143
  ```bash
144
  docker build -t material-forge .
145
  docker run -p 7860:7860 material-forge
146
  ```
147
 
148
  ### Local Development
 
149
  ```bash
150
- # Install dependencies
151
  uv sync
152
-
153
- # Start the environment server
154
  uv run server
 
 
 
155
 
156
- # Run the inference agent (requires HF_TOKEN)
157
  set HF_TOKEN=your_token
158
  uv run python inference.py
159
  ```
160
 
161
- ---
162
-
163
  ## Project Structure
164
- ```
 
165
  MaterialForge/
166
- +-- environment/ # Core Physics & Discovery Logic
167
- +-- server/ # FastAPI Server & Static Assets
168
- +-- scripts/ # Evaluation & Automation scripts
169
- +-- tests/ # Unit tests & Benchmark suites
170
- +-- outputs/ # Execution logs & Benchmark results
171
- +-- models.py # OpenEnv Data Models
172
- +-- inference.py # Main Agent entry point
173
- +-- openenv.yaml # Discovery task definitions
174
- +-- Dockerfile # Space deployment config
175
- +-- pyproject.toml # Dependency management
176
- +-- uv.lock # Deterministic lockfile
177
- +-- client.py # Environment API client
 
 
178
  ```
179
 
180
- ---
 
 
 
 
 
 
 
 
 
 
181
 
182
  <div align="center">
183
  <p>Built for the <b>Meta PyTorch OpenEnv Hackathon</b> by Arsh Pathan</p>
 
1
  ---
2
  title: MaterialForge
3
+ emoji: "🔬"
4
  colorFrom: red
5
  colorTo: blue
6
  sdk: docker
 
19
  [![Docker Ready](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/)
20
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
21
 
22
+ MaterialForge is an OpenEnv-compatible reinforcement learning environment where an agent must **design crystalline structures step by step** to match target physical properties under a finite budget. The environment is built to train and evaluate LLM agents on a scientific workflow rather than a static prompt task.
23
 
24
+ ## Live Links
25
 
26
  | Feature | URL |
27
+ |--|--|
28
+ | Environment API | https://huggingface.co/spaces/ArshPathan/material_forge_env |
29
+ | Interactive Dashboard | https://huggingface.co/spaces/ArshPathan/material_forge_env/playground |
30
+ | Training Blog | [BLOG.md](BLOG.md) |
31
+ | Research / Strategy Notes | [RESEARCH.md](RESEARCH.md) |
32
+ | Training Notebook | [training/MaterialForge_GRPO_Training.ipynb](training/MaterialForge_GRPO_Training.ipynb) |
33
+
34
+ Quick health check:
35
 
 
36
  ```bash
37
  curl https://huggingface.co/spaces/ArshPathan/material_forge_env/health
38
  ```
39
 
40
+ ## Problem
41
 
42
+ The core task is:
43
 
44
+ **Given target values for hardness, conductivity, thermal resistance, and elasticity, can an agent construct a stable, ordered crystal lattice that matches those targets without wasting budget?**
45
 
46
+ The agent does not solve this in one shot. It must:
 
 
 
 
47
 
48
+ - observe the current 8x8 lattice state
49
+ - choose an action
50
+ - see how the action changes material properties
51
+ - recover from overshooting and bad structure
52
+ - build toward a high-quality final phase
53
 
54
+ This makes MaterialForge a good fit for:
55
 
56
+ - **Theme #3.1: World Modeling**
57
+ - **Theme #2: Long-Horizon Planning**
 
58
 
59
+ Those theme choices are directly aligned with the hackathon guidance in `docs/` and the internal project strategy in [RESEARCH.md](RESEARCH.md).
60
 
61
+ ## Why This Matters
62
 
63
+ MaterialForge is inspired by **inverse material design**. Even though the simulation is simplified to an 8x8 lattice, the environment captures several real scientific ideas:
64
 
65
+ - **Percolation pathways** for conductivity
66
+ - **Coordination-driven stability** for local structure quality
67
+ - **Phase / order formation** for crystalline vs amorphous behavior
68
+ - **Budget-constrained design** where good science is not only about accuracy but also efficiency
69
 
70
+ The result is a training environment where the model must do more than fill a grid. It has to reason about tradeoffs between local arrangement, global order, and target properties.
71
+
72
+ ## What the Agent Sees and Does
73
+
74
+ ### Observation Space
75
+
76
+ | Field | Description |
77
+ |--|--|
78
+ | `grid` | 8x8 atomic lattice containing `A`, `B`, `C`, `P`, or `.` |
79
+ | `target` | Target values for hardness, conductivity, thermal resistance, elasticity |
80
+ | `current_properties` | Current estimated properties of the lattice |
81
+ | `phase` | Structural phase, such as amorphous or crystalline |
82
+ | `total_cost` | Current budget spent |
83
+ | `cost_budget` | Maximum budget for the episode |
84
+ | `step_number` | Current step count |
85
+ | `score_breakdown` | Structural stability and order metrics |
86
+
87
+ ### Action Space
88
+
89
+ | Action | Description |
90
+ |--|--|
91
+ | `place` | Place an atom on an empty cell |
92
+ | `replace` | Replace one atom species with another |
93
+ | `remove` | Remove an atom from the grid |
94
+
95
+ ### Atom Types
96
+
97
+ | Symbol | Role | Primary Strength | Cost |
98
+ |--|--|--|--|
99
+ | `A` | Metal | Hardness | 8 |
100
+ | `B` | Conductor | Conductivity | 6 |
101
+ | `C` | Ceramic | Thermal resistance | 4 |
102
+ | `P` | Polymer | Elasticity | 2 |
103
+
104
+ ## Reward Design
105
+
106
+ The environment uses a dense scientific reward made of four parts:
107
+
108
+ 1. **Property Matching (50%)**
109
+ The closer the current lattice is to the target property vector, the higher the reward.
110
+ 2. **Structural Stability (25%)**
111
+ Rewards clustered, well-coordinated structures and penalizes fragile or isolated atoms.
112
+ 3. **Lattice Order (15%)**
113
+ Encourages periodic, symmetry-friendly layouts instead of scattered placements.
114
+ 4. **Phase Bonus (10%)**
115
+ Rewards transitions toward ordered crystalline phases.
116
+
117
+ All rewards are normalized to the `[0, 1]` range.
118
+
119
+ ## Why Judges Should Care
120
+
121
+ MaterialForge is designed to score well on the hackathon dimensions that matter most:
122
+
123
+ - **Environment Innovation**
124
+ It places RL training into a scientific design workflow instead of a toy game.
125
+ - **Storytelling**
126
+ The project has a strong narrative: an AI scientist building materials inside a lab-style dashboard.
127
+ - **Showing Improvement**
128
+ The repo includes a training notebook, saved run artifacts, and training plots.
129
+ - **Reward / Pipeline Coherence**
130
+ The environment, rubric, and training setup are aligned around property matching, structure quality, and efficiency.
131
+
132
+ ## Architecture
133
+
134
+ MaterialForge follows a decoupled architecture:
135
+
136
+ ```text
137
+ Agent / Training Notebook
138
+ -> HTTP reset / step calls or TRL tool-calling wrapper
139
+ FastAPI Server (server/app.py)
140
+ -> OpenEnv-compatible environment runtime
141
+ Environment Wrapper (server/material_forge_env_environment.py)
142
+ -> lattice state + reward / termination logic
143
+ Physics Engine (environment/physics.py)
144
+ -> property estimates, phase classification, structural scoring
145
+ Dashboard (server/static/)
146
+ -> interactive lab visualization for judges and demos
147
  ```
148
 
149
+ Key implementation files:
 
 
 
150
 
151
+ - `server/material_forge_env_environment.py`
152
+ - `environment/physics.py`
153
+ - `environment/lattice.py`
154
+ - `environment/rubrics.py`
155
+ - `training/MaterialForge_GRPO_Training.ipynb`
156
+ - `inference.py`
157
 
158
+ ## Training Approach
159
 
160
+ We used a GRPO-based notebook pipeline built with:
161
 
162
+ - **OpenEnv** for environment structure
163
+ - **TRL** for RL post-training
164
+ - **Unsloth** for efficient 4-bit QLoRA finetuning
165
+ - **Qwen** family models for tool-using policy training
 
 
 
 
166
 
167
+ For the **Grand Finale Submission**, we scaled the training to **Qwen2.5-7B-Instruct** using a high-memory server with **142GB VRAM**, enabling **8 generations per prompt** and real-time gradient updates (Accumulation=1) for maximum stability.
168
 
169
+ ## Training Evidence
 
 
 
 
170
 
171
+ While we provide historical artifacts from **Run - V** (0.6B model) as a baseline for rapid iteration, the final submission policy is derived from the **Grand Finale** run:
172
 
173
+ ### Grand Finale Run (Final Submission)
174
+ - **Base Model:** `Qwen2.5-7B-Instruct`
175
+ - **Generations per Prompt:** `8`
176
+ - **Max Completion Length:** `512`
177
+ - **Spatial Reward Bonus:** `0.15`
178
+ - **Hardware:** High-memory server (142GB VRAM)
179
 
180
+ ### Historical Baseline (Run - V)
181
+ - **Base Model:** `Qwen/Qwen3-0.6B`
182
+ - **Dataset Size:** `100` episodes
183
+ - **Trainable Parameters:** `10,092,544`
184
+ - **Trainable Fraction:** `1.67%`
185
+ - **Hardware:** `1x NVIDIA L40S`
186
+ - **Random Baseline Mean Reward:** `0.5525`
187
+ - **Random Baseline Mean Best Reward:** `0.6137`
188
 
189
+ ### Reward Curve
 
 
 
190
 
191
+ ![Run V Reward Curve](training/runs/Run%20-%20V/reward_curve.png)
 
192
 
193
+ ### Loss Curve
194
 
195
+ ![Run V Loss Curve](training/runs/Run%20-%20V/loss_curve.png)
196
 
197
+ ### Baseline Comparison
198
 
199
+ ![Run V Baseline Comparison](training/runs/Run%20-%20V/baseline_comparison.png)
 
 
200
 
201
+ These artifacts are also referenced in the project blog:
202
 
203
+ - [BLOG.md](BLOG.md)
204
 
205
+ ## Submission Materials
 
 
 
 
206
 
207
+ This repo now contains the core materials judges need:
208
+
209
+ - OpenEnv-compatible environment
210
+ - Hugging Face Space deployment target
211
+ - training notebook using TRL + Unsloth
212
+ - saved historical training artifacts
213
+ - project blog / write-up
214
+ - research and strategy notes
215
+
216
+ Recommended review order for judges:
217
+
218
+ 1. Read this README
219
+ 2. Open the live Space / dashboard
220
+ 3. Review [BLOG.md](BLOG.md)
221
+ 4. Inspect [training/MaterialForge_GRPO_Training.ipynb](training/MaterialForge_GRPO_Training.ipynb)
222
+ 5. Inspect `training/runs/Run - V/` for plots
223
 
224
  ## Quickstart
225
 
226
+ ### Docker
227
+
228
  ```bash
229
  docker build -t material-forge .
230
  docker run -p 7860:7860 material-forge
231
  ```
232
 
233
  ### Local Development
234
+
235
  ```bash
 
236
  uv sync
 
 
237
  uv run server
238
+ ```
239
+
240
+ ### Run the Inference Agent
241
 
242
+ ```bash
243
  set HF_TOKEN=your_token
244
  uv run python inference.py
245
  ```
246
 
 
 
247
  ## Project Structure
248
+
249
+ ```text
250
  MaterialForge/
251
+ ├── docs/ # Hackathon guidance and external reference notes
252
+ ├── environment/ # Physics, lattice state, reward config
253
+ ├── scenarios/ # Target scenario generation
254
+ ├── server/ # FastAPI server + dashboard assets
255
+ ├── tests/ # Benchmarking helpers
256
+ ├── training/ # GRPO notebook + archived run artifacts
257
+ ├── BLOG.md # Judge-facing training write-up
258
+ ├── README.md # Project overview
259
+ ├── RESEARCH.md # Internal strategy and theme alignment
260
+ ├── inference.py # Baseline inference agent
261
+ ├── client.py # Environment client
262
+ ├── models.py # Pydantic action / observation models
263
+ ├── openenv.yaml # OpenEnv task manifest
264
+ └── Dockerfile # Deployment image
265
  ```
266
 
267
+ ## Final Summary
268
+
269
+ MaterialForge is our attempt to show that RL environments for LLMs can move beyond toy games into structured scientific workflows. It combines:
270
+
271
+ - a novel environment
272
+ - verifiable rewards
273
+ - long-horizon action sequences
274
+ - a live demo surface
275
+ - and a practical RL training pipeline
276
+
277
+ The project is not just about placing atoms on a grid. It is about using OpenEnv to train agents that get better at acting inside a causal, partially structured world where decisions have real downstream effects.
278
 
279
  <div align="center">
280
  <p>Built for the <b>Meta PyTorch OpenEnv Hackathon</b> by Arsh Pathan</p>
blog_assets/run_v_baseline_comparison.png ADDED
blog_assets/run_v_loss_curve.png ADDED
training/MaterialForge_GRPO_Training.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
training/runs/Run - V/baseline_comparison.png ADDED
training/runs/Run - V/loss_curve.png ADDED
training/runs/Run - VI/baseline_comparison.png ADDED
training/runs/Run - VI/loss_curve.png ADDED
training/train_env_wrapper.py CHANGED
@@ -36,7 +36,7 @@ def get_curriculum_difficulty() -> str:
36
  return difficulty
37
 
38
 
39
- class MaterialForgeTRLEnv:
40
  """TRL-compatible wrapper for MaterialForge crystal design."""
41
 
42
  def __init__(self):
@@ -45,13 +45,13 @@ class MaterialForgeTRLEnv:
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
- self._best_phase = "amorphous"
53
- self._best_rows_used = 0
54
- self._best_cols_used = 0
55
 
56
  def reset(self, **kwargs) -> str:
57
  """Resets the environment with a curriculum-based difficulty."""
@@ -64,52 +64,52 @@ class MaterialForgeTRLEnv:
64
  self.done = False
65
 
66
  # Reset counters
67
- self._best_reward = 0.0
68
- self._invalid_actions = 0
69
- self._total_actions = 0
70
- self._best_phase = obs.phase
71
- self._best_rows_used = 0
72
- self._best_cols_used = 0
73
-
74
- return self._format_observation(obs)
75
-
76
- def place_atom(self, row: int, col: int, atom: str) -> str:
77
- """Place an atom on an empty cell of the crystal lattice.
78
-
79
- Args:
80
- row: Row index on the 8x8 grid (0-7)
81
- col: Column index on the 8x8 grid (0-7)
82
- 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)
83
-
84
- Returns:
85
- Updated lattice state with current properties, target gaps, and phase.
86
- """
87
- return self._do_step("place", row, col, atom)
88
-
89
- def remove_atom(self, row: int, col: int) -> str:
90
- """Remove an atom from the crystal lattice to free up budget.
91
-
92
- Args:
93
- row: Row index on the 8x8 grid (0-7)
94
- col: Column index on the 8x8 grid (0-7)
95
-
96
- Returns:
97
- Updated lattice state with current properties, target gaps, and phase.
98
- """
99
- return self._do_step("remove", row, col, None)
100
-
101
- def replace_atom(self, row: int, col: int, atom: str) -> str:
102
- """Replace an existing atom with a different species to adjust material properties.
103
-
104
- Args:
105
- row: Row index on the 8x8 grid (0-7)
106
- col: Column index on the 8x8 grid (0-7)
107
- atom: New atom species - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)
108
-
109
- Returns:
110
- Updated lattice state with current properties, target gaps, and phase.
111
- """
112
- return self._do_step("replace", row, col, atom)
113
 
114
  def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:
115
  if self.done:
@@ -117,13 +117,13 @@ class MaterialForgeTRLEnv:
117
 
118
  self._total_actions += 1
119
 
120
- try:
121
- row = max(0, min(int(row), GRID_SIZE - 1))
122
- col = max(0, min(int(col), GRID_SIZE - 1))
123
- except (ValueError, TypeError):
124
- self._invalid_actions += 1
125
- self.reward = self._compute_episode_reward()
126
- return "ERROR: Coordinates must be integers 0-7."
127
 
128
  if atom is not None:
129
  atom = str(atom).upper().strip()
@@ -132,75 +132,75 @@ class MaterialForgeTRLEnv:
132
 
133
  # --- Invalid Action Pre-check ---
134
  grid = self._obs.grid if self._obs else None
135
- if grid:
136
- cell = grid[row][col]
137
- if action_type == "place" and cell != ".":
138
- self._invalid_actions += 1
139
- self.reward = self._compute_episode_reward()
140
- return (
141
- f"INVALID ACTION: ({row},{col}) is already occupied by '{cell}'. "
142
- "Use replace_atom.\n" + self._format_observation(self._obs)
143
- )
144
-
145
- if action_type == "remove" and cell == ".":
146
- self._invalid_actions += 1
147
- self.reward = self._compute_episode_reward()
148
- return (
149
- f"INVALID ACTION: Cannot remove from empty cell ({row},{col}).\n"
150
- + self._format_observation(self._obs)
151
- )
152
-
153
- if action_type == "replace" and (cell == "." or cell == atom):
154
- self._invalid_actions += 1
155
- self.reward = self._compute_episode_reward()
156
- return (
157
- f"INVALID ACTION: replace_atom at ({row},{col}) is redundant or invalid.\n"
158
- + self._format_observation(self._obs)
159
- )
160
 
161
  action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)
162
- obs = self.env.step(action)
163
- self._obs = obs
164
-
165
- # Cache the structure associated with the best reward so shaping stays coherent.
166
- step_reward = obs.reward if obs.reward is not None else 0.0
167
- if step_reward >= self._best_reward:
168
- self._best_reward = step_reward
169
- self._best_rows_used, self._best_cols_used = self._count_rows_and_cols(obs.grid)
170
- self._best_phase = obs.phase
171
-
172
- self.reward = self._compute_episode_reward()
173
-
174
- self.done = obs.done
175
- return self._format_observation(obs)
176
-
177
- def _count_rows_and_cols(self, grid: list[list[str]]) -> tuple[int, int]:
178
- rows_used = set()
179
- cols_used = set()
180
- for r, row in enumerate(grid):
181
- for c, cell in enumerate(row):
182
- if cell != ".":
183
- rows_used.add(r)
184
- cols_used.add(c)
185
- return len(rows_used), len(cols_used)
186
-
187
- def _compute_episode_reward(self) -> float:
188
- total = max(self._total_actions, 1)
189
- invalid_ratio = self._invalid_actions / total
190
- invalid_penalty = 0.3 * invalid_ratio
191
-
192
- row_spread = min(self._best_rows_used / 4.0, 1.0)
193
- col_spread = min(self._best_cols_used / 4.0, 1.0)
194
- spatial_bonus = 0.10 * (row_spread * col_spread)
195
-
196
- phase_bonus = 0.0
197
- if self._best_phase == "crystalline":
198
- phase_bonus = 0.15
199
- elif self._best_phase == "polycrystalline":
200
- phase_bonus = 0.05
201
-
202
- final = self._best_reward + spatial_bonus + phase_bonus - invalid_penalty
203
- return max(min(final, 1.0), 0.0)
204
 
205
  def _format_observation(self, obs: MaterialForgeObservation) -> str:
206
  grid_lines = []
@@ -210,14 +210,14 @@ class MaterialForgeTRLEnv:
210
  gaps = ", ".join([f"{p}={obs.current_properties.get(p,0):.1f}/{obs.target.get(p,0):.1f}" for p in PROPERTY_NAMES])
211
  sb = obs.score_breakdown or {}
212
 
213
- return (
214
- f"Step {obs.step_number}/{obs.max_steps} | Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | Phase: {obs.phase}\n"
215
- f"Reward: {obs.reward:.4f} | Properties: {gaps}\n"
216
- f"Stability: {sb.get('structural_stability',0):.3f} | Order: {sb.get('lattice_order_index',0):.3f}\n"
217
- f"Grid:\n{chr(10).join(grid_lines)}"
218
- )
219
-
220
-
221
- def reward_func(environments, **kwargs) -> list[float]:
222
- """Episode reward using the best-scoring state plus aligned shaping penalties."""
223
- return [env._compute_episode_reward() for env in environments]
 
36
  return difficulty
37
 
38
 
39
+ class MaterialForgeTRLEnv:
40
  """TRL-compatible wrapper for MaterialForge crystal design."""
41
 
42
  def __init__(self):
 
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
+ self._best_phase = "amorphous"
53
+ self._best_rows_used = 0
54
+ self._best_cols_used = 0
55
 
56
  def reset(self, **kwargs) -> str:
57
  """Resets the environment with a curriculum-based difficulty."""
 
64
  self.done = False
65
 
66
  # Reset counters
67
+ self._best_reward = 0.0
68
+ self._invalid_actions = 0
69
+ self._total_actions = 0
70
+ self._best_phase = obs.phase
71
+ self._best_rows_used = 0
72
+ self._best_cols_used = 0
73
+
74
+ return self._format_observation(obs)
75
+
76
+ def place_atom(self, row: int, col: int, atom: str) -> str:
77
+ """Place an atom on an empty cell of the crystal lattice.
78
+
79
+ Args:
80
+ row: Row index on the 8x8 grid (0-7)
81
+ col: Column index on the 8x8 grid (0-7)
82
+ 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)
83
+
84
+ Returns:
85
+ Updated lattice state with current properties, target gaps, and phase.
86
+ """
87
+ return self._do_step("place", row, col, atom)
88
+
89
+ def remove_atom(self, row: int, col: int) -> str:
90
+ """Remove an atom from the crystal lattice to free up budget.
91
+
92
+ Args:
93
+ row: Row index on the 8x8 grid (0-7)
94
+ col: Column index on the 8x8 grid (0-7)
95
+
96
+ Returns:
97
+ Updated lattice state with current properties, target gaps, and phase.
98
+ """
99
+ return self._do_step("remove", row, col, None)
100
+
101
+ def replace_atom(self, row: int, col: int, atom: str) -> str:
102
+ """Replace an existing atom with a different species to adjust material properties.
103
+
104
+ Args:
105
+ row: Row index on the 8x8 grid (0-7)
106
+ col: Column index on the 8x8 grid (0-7)
107
+ atom: New atom species - 'A' (Metal), 'B' (Conductor), 'C' (Ceramic), or 'P' (Polymer)
108
+
109
+ Returns:
110
+ Updated lattice state with current properties, target gaps, and phase.
111
+ """
112
+ return self._do_step("replace", row, col, atom)
113
 
114
  def _do_step(self, action_type: str, row: int, col: int, atom: Optional[str]) -> str:
115
  if self.done:
 
117
 
118
  self._total_actions += 1
119
 
120
+ try:
121
+ row = max(0, min(int(row), GRID_SIZE - 1))
122
+ col = max(0, min(int(col), GRID_SIZE - 1))
123
+ except (ValueError, TypeError):
124
+ self._invalid_actions += 1
125
+ self.reward = self._compute_episode_reward()
126
+ return "ERROR: Coordinates must be integers 0-7."
127
 
128
  if atom is not None:
129
  atom = str(atom).upper().strip()
 
132
 
133
  # --- Invalid Action Pre-check ---
134
  grid = self._obs.grid if self._obs else None
135
+ if grid:
136
+ cell = grid[row][col]
137
+ if action_type == "place" and cell != ".":
138
+ self._invalid_actions += 1
139
+ self.reward = self._compute_episode_reward()
140
+ return (
141
+ f"INVALID ACTION: ({row},{col}) is already occupied by '{cell}'. "
142
+ "Use replace_atom.\n" + self._format_observation(self._obs)
143
+ )
144
+
145
+ if action_type == "remove" and cell == ".":
146
+ self._invalid_actions += 1
147
+ self.reward = self._compute_episode_reward()
148
+ return (
149
+ f"INVALID ACTION: Cannot remove from empty cell ({row},{col}).\n"
150
+ + self._format_observation(self._obs)
151
+ )
152
+
153
+ if action_type == "replace" and (cell == "." or cell == atom):
154
+ self._invalid_actions += 1
155
+ self.reward = self._compute_episode_reward()
156
+ return (
157
+ f"INVALID ACTION: replace_atom at ({row},{col}) is redundant or invalid.\n"
158
+ + self._format_observation(self._obs)
159
+ )
160
 
161
  action = MaterialForgeAction(action_type=action_type, row=row, col=col, atom=atom)
162
+ obs = self.env.step(action)
163
+ self._obs = obs
164
+
165
+ # Cache the structure associated with the best reward so shaping stays coherent.
166
+ step_reward = obs.reward if obs.reward is not None else 0.0
167
+ if step_reward >= self._best_reward:
168
+ self._best_reward = step_reward
169
+ self._best_rows_used, self._best_cols_used = self._count_rows_and_cols(obs.grid)
170
+ self._best_phase = obs.phase
171
+
172
+ self.reward = self._compute_episode_reward()
173
+
174
+ self.done = obs.done
175
+ return self._format_observation(obs)
176
+
177
+ def _count_rows_and_cols(self, grid: list[list[str]]) -> tuple[int, int]:
178
+ rows_used = set()
179
+ cols_used = set()
180
+ for r, row in enumerate(grid):
181
+ for c, cell in enumerate(row):
182
+ if cell != ".":
183
+ rows_used.add(r)
184
+ cols_used.add(c)
185
+ return len(rows_used), len(cols_used)
186
+
187
+ def _compute_episode_reward(self) -> float:
188
+ total = max(self._total_actions, 1)
189
+ invalid_ratio = self._invalid_actions / total
190
+ invalid_penalty = 0.3 * invalid_ratio
191
+
192
+ row_spread = min(self._best_rows_used / 4.0, 1.0)
193
+ col_spread = min(self._best_cols_used / 4.0, 1.0)
194
+ spatial_bonus = 0.15 * (row_spread * col_spread)
195
+
196
+ phase_bonus = 0.0
197
+ if self._best_phase == "crystalline":
198
+ phase_bonus = 0.20
199
+ elif self._best_phase == "polycrystalline":
200
+ phase_bonus = 0.10
201
+
202
+ final = self._best_reward + spatial_bonus + phase_bonus - invalid_penalty
203
+ return max(min(final, 1.0), 0.0)
204
 
205
  def _format_observation(self, obs: MaterialForgeObservation) -> str:
206
  grid_lines = []
 
210
  gaps = ", ".join([f"{p}={obs.current_properties.get(p,0):.1f}/{obs.target.get(p,0):.1f}" for p in PROPERTY_NAMES])
211
  sb = obs.score_breakdown or {}
212
 
213
+ return (
214
+ f"Step {obs.step_number}/{obs.max_steps} | Cost: {obs.total_cost:.0f}/{obs.cost_budget:.0f} | Phase: {obs.phase}\n"
215
+ f"Reward: {obs.reward:.4f} | Properties: {gaps}\n"
216
+ f"Stability: {sb.get('structural_stability',0):.3f} | Order: {sb.get('lattice_order_index',0):.3f}\n"
217
+ f"Grid:\n{chr(10).join(grid_lines)}"
218
+ )
219
+
220
+
221
+ def reward_func(environments, **kwargs) -> list[float]:
222
+ """Episode reward using the best-scoring state plus aligned shaping penalties."""
223
+ return [env._compute_episode_reward() for env in environments]