Spaces:
Sleeping
Sleeping
Commit ·
ff23f69
1
Parent(s): e21b468
Upload folder using huggingface_hub
Browse files- .gitattributes +5 -0
- BLOG.md +181 -0
- README.md +196 -99
- blog_assets/run_v_baseline_comparison.png +0 -0
- blog_assets/run_v_loss_curve.png +0 -0
- training/MaterialForge_GRPO_Training.ipynb +0 -0
- training/runs/Run - V/baseline_comparison.png +0 -0
- training/runs/Run - V/loss_curve.png +0 -0
- training/runs/Run - VI/baseline_comparison.png +0 -0
- training/runs/Run - VI/loss_curve.png +0 -0
- training/train_env_wrapper.py +139 -139
.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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
[](https://www.docker.com/)
|
| 20 |
[](LICENSE)
|
| 21 |
|
| 22 |
-
-
|
| 23 |
|
| 24 |
-
##
|
| 25 |
|
| 26 |
| Feature | URL |
|
| 27 |
-
|--|--
|
| 28 |
-
|
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
**Quick health check:**
|
| 32 |
```bash
|
| 33 |
curl https://huggingface.co/spaces/ArshPathan/material_forge_env/health
|
| 34 |
```
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
|
| 52 |
-
- **
|
| 53 |
-
- **
|
| 54 |
-
- **Phase Transition Engineering**: Agents must navigate the complex trade-offs between entropy (amorphicity) and order (crystallinity).
|
| 55 |
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
```
|
| 75 |
|
| 76 |
-
|
| 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 |
-
##
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 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 |
-
|
| 97 |
|
| 98 |
-
|
| 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 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
-
|
| 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 |
-
|
| 116 |
-
All rewards are normalized to the `[0.0, 1.0]` range for standardized agent evaluation.
|
| 117 |
|
| 118 |
-
|
| 119 |
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
|
| 124 |
-
|
| 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 |
-
|
| 131 |
|
| 132 |
-
|
| 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
|
|
|
|
| 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 |
-
|
| 157 |
set HF_TOKEN=your_token
|
| 158 |
uv run python inference.py
|
| 159 |
```
|
| 160 |
|
| 161 |
-
---
|
| 162 |
-
|
| 163 |
## Project Structure
|
| 164 |
-
|
|
|
|
| 165 |
MaterialForge/
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 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 |
[](https://www.docker.com/)
|
| 20 |
[](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 |
+

|
|
|
|
| 192 |
|
| 193 |
+
### Loss Curve
|
| 194 |
|
| 195 |
+

|
| 196 |
|
| 197 |
+
### Baseline Comparison
|
| 198 |
|
| 199 |
+

|
|
|
|
|
|
|
| 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.
|
| 195 |
-
|
| 196 |
-
phase_bonus = 0.0
|
| 197 |
-
if self._best_phase == "crystalline":
|
| 198 |
-
phase_bonus = 0.
|
| 199 |
-
elif self._best_phase == "polycrystalline":
|
| 200 |
-
phase_bonus = 0.
|
| 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]
|