Adhitya122 commited on
Commit
5511b8f
·
verified ·
1 Parent(s): d5ef2b7

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/Logs.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/molforge_architecture.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/reward_curve.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,163 +1,214 @@
1
  ---
2
- title: MolForge
3
- emoji: 🧪
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: docker
7
- app_port: 8000
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- # MolForge
11
 
12
- This repository implements an OpenEnv-compatible reinforcement learning environment for **medicinal chemistry lead optimization**. The agent does not directly see the true biological properties of the candidate molecule. Instead, a specialist team iteratively edits a KRAS G12C candidate under limited assay budget, partial observability, and strict safety constraints, receiving a noisy simulated output, and is rewarded for discovering a highly potent, synthesizable, and safe drug candidate.
13
 
14
- The environment is designed as a **partially observable Markov decision process (POMDP)** with:
15
- - hidden ground-truth molecular properties and scenario constraints
16
- - hidden target mutation traps (e.g. KRAS resistance panel shifts)
17
- - visible task metadata, team communication, assay results, and remaining budget
18
- - simulated `RDKit` descriptors and `TDC` (Therapeutics Data Commons) predictions (QED, SA_Score, LogP, TPSA)
19
- - dense step-wise reward (in curriculum mode) plus terminal reward for submission quality
20
 
21
- At a high level, each episode looks like this:
22
- 1. `reset()` picks a biological scenario (e.g. `level_1_medium`) and seeds the simulator.
23
- 2. The agent receives a `MolForgeObservation` describing the task, the starting molecule scaffold, and the current visible state.
24
- 3. The agent (acting as different roles) submits a `MolForgeAction` such as `edit`, `run_assay`, `propose_nomination`, or `submit`.
25
- 4. The **Governance rule engine** checks whether the action is valid, requiring multi-agent consensus for final decisions.
26
- 5. The transition engine updates the molecule, spends the assay budget, and returns oracle readings.
27
- 6. The reward computer scores the step based on whether the action was invalid, vetoed, or successful.
28
- 7. The environment returns a new observation with updated history, assay readings, and reward.
29
- 8. The episode ends when the agent successfully submits the molecule, exhausts its budget, or reaches the maximum step horizon.
30
 
31
- ---
32
 
33
- ## Hidden state vs Visible state
 
 
 
34
 
35
- ### Hidden state
36
- The simulator keeps ground-truth properties that the agent never directly sees. It contains:
37
- - The true underlying scoring functions for `potency`, `safety`, and `synthesizability`.
38
- - Sunk-cost traps and late-stage target mutations (e.g., in `level_2_hard`).
39
- - The strict constraints required for a valid submission.
40
- - The remaining hidden milestones for the scenario.
41
 
42
- ### Visible state
43
- The agent only sees `MolForgeObservation`, which includes:
44
- - The current `TaskSpec` and `scenario_id`.
45
- - Pipeline history and previous actions.
46
- - The current molecular scaffold (in SMILES format).
47
- - The `budget_used` and `remaining_budget`.
48
- - Responses from the `run_assay` oracle (TDC predictors and RDKit descriptors).
49
- - The `GovernanceStatus` showing which specialist agents have approved or objected.
50
- - The `step_reward_breakdown`.
51
 
52
- This separation is what makes the environment a POMDP rather than a fully observed simulator.
53
 
54
- ---
55
 
56
- ## Repository files navigation
57
-
58
- ### `models.py`
59
- Defines the Pydantic contracts that all other modules use:
60
- - `MolForgeAction`: One structured step chosen by the agent. Fields include `action_type`, `acting_role`, `tool_name`, `slot`, `fragment`, and `rationale`.
61
- - `MolForgeObservation`: What the agent can see after each step; includes `current_molecule`, `last_transition_summary`, `reward_breakdown`, and `governance_status`.
62
- - `MolForgeState`: The internal tracked state including `episode_id`, `step_count`, and `invalid_action_count`.
63
-
64
- ### `server/scenarios.py`
65
- This is where episodes come from. It defines a curated library of three biological scenarios, each bundling a starting scaffold, a budget, and a specific molecular target:
66
- - `level_0_easy`: Potency-first optimization with a generous budget and a starting scaffold that is one or two edits from success.
67
- - `level_1_medium`: Multi-objective optimization with safety as a hard constraint and moderate budget pressure.
68
- - `level_2_hard`: A sunk-cost trap plus late target mutation. The initial scaffold family has a hidden liability, and the best policy is often to restart early.
69
-
70
- ### `server/actions.py` & `server/governance.py`
71
- The rule engines enforcing scientific and procedural constraints before each action is applied:
72
- - `run_assay`: Costs budget. Assembles the fragments into a valid `SMILES` string and evaluates the current molecule using `TDC` Oracles and `RDKit` logic (e.g. `MolLogP`, `TPSA`, `NumRotatableBonds`, `QED`).
73
- - `edit`: Replaces a specific R-group slot (`warhead`, `hinge`, `solvent_tail`, `back_pocket`) with a new chemical fragment (e.g. `acrylamide`, `fluorophenyl`, `morpholine`). Clears previously gathered evidence.
74
- - `submit`: Ends the episode. Triggers the final evaluation grader against the scenario's strict hard constraints (`potency_min`, `toxicity_max`, `synth_min`).
75
- - **Governance**: Certain actions require multi-agent consensus. If the `Lead Chemist` tries to submit without the `Safety Specialist`'s approval, the action is vetoed.
76
-
77
- ### `server/molforge_environment.py`
78
- This is the orchestration layer that ties everything together.
79
- On `reset()` it:
80
- - Generates a task scenario.
81
- - Clears the message log, history, and resets the molecule to the default scaffold.
82
-
83
- On `step()` it:
84
- - Checks governance rules and validates the action.
85
- - Executes the action (e.g. replacing an R-group fragment or running an assay).
86
- - Computes reward (via Curriculum or Assay-Gated mode).
87
- - Builds the next `MolForgeObservation`.
88
 
89
  ---
90
 
91
- ## What actually happens on one step
92
- Here is the concrete order of operations for `env.step(action)`:
93
- 1. Increment the step counter.
94
- 2. Run validation checks. If the action format is invalid, return a failure report and a `-1.0` reward.
95
- 3. Assess **Governance**. If a required specialist agent vetoes the action, the action is blocked and penalized.
96
- 4. Execute the action (`edit`, `run_assay`, `submit`).
97
- 5. Deduct oracle budget if `run_assay` was called.
98
- 6. Compute decomposed reward from the state transition (e.g., getting penalized for redundant assays).
99
- 7. If the episode is ending (via `submit`, max steps, or zero budget), compute the terminal `submission_score`.
100
- 8. Return an observation that exposes the visible summary but not the hidden truth.
101
 
102
- ---
103
 
104
- ## Typical successful pipeline
105
- Most scenarios reward a sensible experiment order similar to:
106
- 1. `run_assay` (Assay potency and safety of the baseline molecule).
107
- 2. `edit` (Swap an R-group fragment to improve a weak property).
108
- 3. `run_assay` (Gather new evidence for the modified molecule).
109
- 4. `propose_nomination` (Discuss the findings with the multi-agent review board).
110
- 5. `submit` (Finalize the candidate).
111
 
112
- The exact best sequence depends on the scenario. In `level_2_hard`, the best strategy is often to `restart` the entire scaffold immediately rather than wasting budget on a doomed trajectory.
113
 
114
- ---
 
115
 
116
- ## Reward Strategy & Episode termination
 
117
 
118
- MolForge uses two distinct reward settings for different purposes:
119
 
120
- **1. Training / RL Warmup (`MOLFORGE_REWARD_MODE=curriculum`)**
121
- - Gives partial credit at the end of an episode even if the model didn't submit, provided it gathered useful evidence.
122
- - It actively prevents "reward hacking" by penalizing assay-spamming, and giving massive multipliers to successful submissions.
123
 
124
- **2. Judge-Facing Evaluation (`MOLFORGE_REWARD_MODE=assay_gated`)**
125
- - Strict OpenEnv hackathon rules.
126
- - If the agent does not formally `submit` the candidate, the final score is `0.0`.
127
- - No partial credit is given for just gathering evidence.
128
 
129
- An episode ends when one of the following happens:
130
- - The agent explicitly chooses `submit`.
131
- - Resources (oracle budget) are exhausted.
132
- - The environment reaches `MAX_STEPS`.
133
 
134
- ---
135
 
136
- ## Installation & Usage
137
- The package requires Python ≥ 3.10.
138
- ```bash
139
- pip install "openenv-core[core]>=0.2.3" pydantic transformers trl peft datasets
140
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- ### 1. In-process environment
143
- Use `MolForgeEnvironment` when you want direct Python access with full structured observations:
144
- ```python
145
- from models import MolForgeAction
146
- from server.molforge_environment import MolForgeEnvironment
147
-
148
- env = MolForgeEnvironment()
149
- obs = env.reset()
150
-
151
- action = MolForgeAction(
152
- action_type="run_assay",
153
- acting_role="Lead Chemist",
154
- tool_name="potency_oracle",
155
- rationale="Need to gather baseline potency evidence."
156
- )
157
- obs = env.step(action)
158
- print(obs.reward)
159
- print(obs.last_transition_summary)
160
  ```
161
 
162
- ### 2. RL Training Notebook
163
- We have provided a cleanly documented `issue/molforge_grpo_official_submission.ipynb` which demonstrates exactly how to fine-tune a Qwen3.5 model using TRL's GRPO trainer natively against this OpenEnv environment.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "MolForge: Verifier-Driven RL for Drug Discovery"
3
+ emoji: 🧬
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: static
7
+ pinned: false
8
+ license: mit
9
+ tags:
10
+ - reinforcement-learning
11
+ - drug-discovery
12
+ - chemistry
13
+ - multi-agent
14
+ - oncology
15
+ - molecular-simulation
16
+ - openenv
17
  ---
18
 
19
+ # MolForge: Verifier-Driven RL for Drug Discovery
20
 
21
+ MolForge is a reinforcement learning environment that simulates a **medical oncology discovery lab**. Unlike traditional LLM tasks where the model generates a final answer in one shot, MolForge forces the model to execute the **scientific method** under real-world constraints: budget, toxicity, and synthesis complexity.
22
 
23
+ **[View the MolForge Space Deployment on Hugging Face](https://huggingface.co/spaces/Adhitya122/molforge)**
24
+ **[Try the RL Training Notebook on Google Colab](https://colab.research.google.com/drive/1c6npGkGNbbbd8XFNeS6zInBpopLnJ4W4?usp=sharing)**
 
 
 
 
25
 
26
+ ### The Scientific Method as a Workflow
 
 
 
 
 
 
 
 
27
 
28
+ Imagine a biotech team tasked with optimizing a lead candidate for **KRAS G12C** (including a high-difficulty resistance panel). The model doesn't just "write" a molecule; it controls a specialist team that must navigate a resource-constrained laboratory:
29
 
30
+ - **Lead Chemist**: Proposes molecular edits and decides when to submit.
31
+ - **Assay Planner**: Allocates limited budget to run empirical tests.
32
+ - **Toxicologist**: Reviews safety risks and can object to unsafe designs.
33
+ - **Process Chemist**: Evaluates whether the molecule is practical to synthesize.
34
 
35
+ Every action—editing a fragment, running a docking simulation, or ordering a toxicity assay—is a decision that impacts the final outcome. The model must learn to gather enough evidence to justify a submission while keeping the project within budget.
 
 
 
 
 
36
 
37
+ > **Core Philosophy:** The LLM is not the judge. The LLM is the scientist being judged by external, verifiable reality.
 
 
 
 
 
 
 
 
38
 
 
39
 
40
+ ## What Makes MolForge Special?
41
 
42
+ MolForge is built to move beyond simple "molecule generation" into "scientific workflow optimization." Here are the seven core pillars that make it unique:
43
+
44
+ 1. [**Verifier-Based Evaluation**](#1-verifier-based-evaluation): The LLM is the scientist, not the judge. It is held accountable by real-world verifiers like **RDKit** and **TDC**.
45
+ 2. [**Chemical & Molecular Simulations**](#2-chemical--molecular-simulations): Realistic simulation of potency and existence using heuristic docking, **RDKit**, and **TDC**.
46
+ 3. [**Self-Correction & Improvement Loop**](#3-self-correction--improvement-loop): After each edit, agents receive structured feedback from verifiers, allowing them to self-correct.
47
+ 4. [**Decomposed Reward Architecture**](#4-decomposed-reward-architecture): Multi-step rewards for every action (research, edits, coordination) provide high observability.
48
+ 5. [**Scientific Model Improvement**](#5-scientific-model-improvement): Real verifier feedback (Reviews) guides the model toward scientifically sound designs.
49
+ 6. [**Strategic Training Modes**](#6-strategic-training-modes): A dual-mode system using **Curriculum mode** (partial credit) and **Assay-Gated mode** (strict).
50
+ 7. [**Multi-Agent Governance**](#7-multi-agent-governance): A specialized team that plans, executes, and shares information to coordinate the next plan of action.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  ---
53
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ ## Architecture
56
 
57
+ The architecture is a closed scientific feedback loop:
 
 
 
 
 
 
58
 
59
+ ![MolForge Architecture](assets/molforge_architecture.png)
60
 
61
+ ### The POMDP Framework: Hidden vs. Visible State
62
+ MolForge is designed as a **partially observable Markov decision process (POMDP)**. This separation is what makes the environment a scientific challenge rather than a simple optimization task.
63
 
64
+ - **Hidden State**: The simulator tracks the ground-truth scoring for `potency`, `safety`, and `synthesizability`. It also hides "sunk-cost traps" and late-stage target mutation shifts (e.g., in `level_2_hard`) that the agent must discover through evidence.
65
+ - **Visible State**: The agent only sees noisy `MolForgeObservation` reports: pipeline history, SMILES scaffolds, remaining budget, and the structured feedback from the verifier assays (RDKit and TDC).
66
 
67
+ ## Scientific Verifier Layers
68
 
69
+ ### RDKit: chemical plausibility
 
 
70
 
71
+ RDKit checks molecule-like behavior and chemistry descriptors. In MolForge, this layer is used to keep the molecule edits grounded in chemical reality. It supports descriptor-style reasoning such as lipophilicity, polarity, tractability, and drug-likeness.
 
 
 
72
 
73
+ ### TDC: biomedical outcome signals
 
 
 
74
 
75
+ Therapeutics Data Commons represents the medical outcome side of the environment. It provides the project with a path toward realistic prediction tasks such as toxicity, synthesis difficulty, and drug-likeness. In the default Docker deployment, RDKit remains active and TDC is optional because it can pull a heavier platform-sensitive ML stack.
76
 
77
+ ### Heuristic docking: receptor fit
78
+
79
+ MolForge includes a docking-style surrogate that answers three fast questions:
80
+
81
+ | Check | Question | Why it matters |
82
+ | --- | --- | --- |
83
+ | Pocket matching | Does the hinge fragment fit the receptor pocket? | Better pocket complementarity improves potency. |
84
+ | Lipophilic match | Is LogP near the target pocket's hydrophobic comfort zone around `3.0`? | Too much lipophilicity can increase toxicity; too little can weaken binding. |
85
+ | Polarity match | Is TPSA near a useful range around `85.0`? | Polarity affects binding, permeability, and clash risk. |
86
+
87
+ This gives the environment fast receptor-aware feedback in milliseconds, which is important for RL.
88
+
89
+ ## Training Story
90
+
91
+ The training pipeline has two stages:
92
+
93
+ 1. **SFT warm start**
94
+ 2. **RL with verifier rewards**
95
+
96
+ ### Base model
97
+
98
+ The model used for the main run is:
99
 
100
+ ```text
101
+ unsloth/Qwen3.5-2B
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  ```
103
 
104
+ The raw base model was not reliable enough for the environment at first. It often failed to produce the exact structured JSON actions that MolForge expects, and it did not consistently respect the specialist-agent interaction format.
105
+
106
+ So the first step was a small SFT warm start. This stage is not meant to teach the model the optimal chemistry. It teaches the model how to speak the environment's action language:
107
+
108
+ - valid JSON actions
109
+ - correct role/action pairing
110
+ - correct molecule slots and fragments
111
+ - concise rationales
112
+ - evidence fields based only on visible observations
113
+ - expected-effect fields such as potency up/down or toxicity up/down
114
+ - valid specialist messages where needed
115
+
116
+ ### Training Results
117
+ After SFT, the policy is trained with GRPO-style RL against MolForge itself. During training, the model explores the 256-combination molecular edit space, receiving rewards for molecule quality, evidence coverage, and budget discipline.
118
+
119
+ ![Reward Curve](assets/reward_curve.png)
120
+ ![Training Logs](assets/Logs.png)
121
+
122
+ ### Performance Comparison: SFT vs. RL
123
+
124
+ | Difficulty | Before (SFT Model) | After RL Training | Improvement |
125
+ | :--- | :---: | :---: | :---: |
126
+ | **Easy** | 0.1167 | 0.1295 | **+10.9%** |
127
+ | **Medium** | 0.1167 | 0.1278 | **+9.5%** |
128
+ | **Hard** | 0.0800 | 0.0866 | **+8.3%** |
129
+
130
+ As shown in the reward curve and logs, the model successfully learns to navigate the scientific constraints, moving from early exploration to consistent, verifier-backed molecule submissions. For strict evaluation, the environment switches back to `assay_gated` mode.
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+ ## Reward Design: Shaping Scientific Behavior
142
+
143
+ The reward function mixes coarse shaping with sparse terminal bonuses to promote rigorous scientific exploration:
144
+
145
+ - **Coarse Feedback**: Edit feedback avoids exposing exact hidden deltas, forcing the model to rely on assays for decision-making.
146
+ - **Information Gain**: Rewards for running useful assays that provide new, evidence-based signal.
147
+ - **Coordination & Governance**: Rewards for correct specialist reviews, proposal discipline, and multi-agent consensus.
148
+ - **Scientific Penalties**: Deductions for invalid actions, repeated states, wasteful assay repetition, and submitting without sufficient potency/safety support.
149
+ - **Terminal Scoring**: A large bonus for submitting a molecule that beats the baseline while satisfying all hard constraints.
150
+
151
+ ### Strategic Training Modes
152
+
153
+ MolForge uses two distinct reward settings to balance training and evaluation:
154
+
155
+ 1. **Curriculum Mode (Training)**: Adds bounded warmup rewards for evidence collection and "near-miss" episodes. It also adds a small **missed-nomination penalty** when a strong evidence package is ready but the agent lets the deadline pass without submitting. This acts as "breadcrumbs" for RL, helping smaller models navigate sparse reward landscapes.
156
+ 2. **Assay-Gated Mode (Evaluation)**: The strict, official hackathon mode. If the agent does not formally `submit` the candidate before the budget is exhausted, the final score is exactly `0.0`. No partial credit is given for just gathering evidence.
157
+
158
+ `final_score` remains the single headline scalar for RL/evaluation. While `candidate_score` and `progress_score` are used for diagnostic observability, the environment is designed so that evidence collection alone cannot look like success; it must lead to a valid submission.
159
+
160
+
161
+
162
+ ## Why This Project Matters
163
+
164
+ MolForge is designed to test a deeper kind of AI capability than simple answer generation. The model must work inside a scientific feedback loop where actions are checked, evidence costs money, unsafe decisions can be blocked, and the final answer only matters if the path to that answer is experimentally justified.
165
+
166
+ The strongest part of the project is that the LLM is not trusted by default. It has to earn trust through verifier-backed decisions.
167
+
168
+ ## Deep Dive: What Makes MolForge Special?
169
+
170
+ ### 1. Verifier-Based Evaluation
171
+ In many LLM systems, the model itself is used as a judge, reviewer, or evaluator. MolForge flips that pattern. The LLM is the scientist being judged, not the judge. It is held accountable by real-world verifiers like **RDKit**, **TDC**, and molecular simulation engines. This ensures that the model's progress is grounded in chemical and biological reality, not just persuasive language.
172
+
173
+ ### 2. Chemical & Molecular Simulations
174
+ MolForge doesn't just predict outcomes; it utilizes multiple simulation layers to ground the model's decisions:
175
+ * **Chemical Plausibility (RDKit):** Decides if the molecule generated by the LLM (via edits) can actually exist in chemical reality. [Visit RDKit](https://www.rdkit.org)
176
+ * **Medical Outcomes (TDC):** Predicts the most probable medical outcomes and properties using the [Therapeutics Data Commons](https://tdcommons.ai).
177
+ * **Heuristic Docking Score:** A fast, physics-inspired simulation that updates **potency** in milliseconds based on three rules:
178
+ 1. **Pocket Matching:** Does the fragment structurally fit the target receptor pocket (e.g., KRAS G12C)?
179
+ 2. **Lipophilic Match:** Is the LogP near the ideal **3.0** to sit comfortably in the hydrophobic pocket?
180
+ 3. **Polarity Match:** Is the TPSA near the ideal **85.0** to avoid repulsive polar clashes?
181
+
182
+ ### 3. Self-Correction & Improvement Loop
183
+ MolForge is an iterative environment. After each proposed molecular modification, the model receives a structured review from the verifiers. This feedback allows the agent to recognize liabilities (like toxicity or low potency) and correct them in the next step. This creates a genuine **self-improvement loop** within each episode.
184
+
185
+ ### 4. Decomposed Reward Architecture
186
+ The reward function is not a single "black box" scalar. We use a multi-step reward system where small-scale rewards are designed for every individual action—research, molecular edits, and inter-agent coordination. While we may output a single total reward for training simplicity (especially for the hackathon), the decomposed components allow for massive observability into which sections of the workflow are lacking.
187
+
188
+ ### 5. Scientific Model Improvement
189
+ We use real verifier feedback to drive model improvement. By providing constant, verifiable reviews, we train the model to improve its designs based on evidence. This moves the model away from simple pattern matching and toward a more rigorous, evidence-based design process.
190
+
191
+ ### 6. Strategic Training Modes: Curriculum vs. Assay-Gated
192
+ To solve the "sparse reward" problem common in RL, MolForge uses two distinct modes:
193
+ * **Curriculum Mode (Training):** If a model fails to submit but showed good scientific behavior, it receives "Partial Credit" (up to +0.75). It gets points for gathering evidence and designing promising molecules. These "breadcrumbs" teach the model how to explore before it discovers the terminal submission bonus.
194
+ * **Assay-Gated Mode (Evaluation):** This is the strict, official mode used for hackathon grading. There is **zero partial credit**. If the model fails to explicitly `submit` a high-potency, safe molecule before the budget runs out, its score is exactly `0.0`.
195
+
196
+ ### 7. Multi-Agent Governance
197
+ Drug discovery is a team effort. MolForge implements a multi-agent system where specialized roles (Lead Chemist, Toxicologist, Assay Planner) review each other's moves, plans, and executions. Crucially, these agents **share information and coordinate** between themselves to decide the next plan of action, ensuring that every decision undergoes a rigorous "peer review" process before execution.
198
+
199
+ ---
200
+
201
+ ## Final Takeaway
202
+
203
+ MolForge is special because it treats the LLM as a trainable research agent inside a controlled scientific environment, not as an oracle. The model is judged by chemistry and biomedical verifiers, corrected by specialist feedback, constrained by assay budget, and scored by a reward system that can explain where the policy succeeded or failed.
204
+
205
+ The important pieces work together:
206
+
207
+ - **Verifier-first evaluation:** RDKit, TDC-style signals, and docking-style simulation judge the model's actions.
208
+ - **Multi-agent review:** specialist roles create checks and balances around each decision.
209
+ - **Self-improvement loop:** every action produces feedback that the next action can respond to.
210
+ - **Decomposed rewards:** the environment tracks molecule quality, evidence, budget, coordination, and safety separately.
211
+ - **Curriculum to strict evaluation:** training can use partial-credit breadcrumbs, while final evaluation remains unforgiving.
212
+ - **Dynamic molecular search:** the model explores 256 fragment combinations across three starting scientific scenarios instead of memorizing one answer.
213
+
214
+ That is the project thesis: useful scientific agents should not merely generate plausible ideas. They should operate in a loop where the world pushes back.
assets/Logs.png ADDED

Git LFS Details

  • SHA256: a5f41fa2250acb308b5d9036fda12eda1e5dd0c98e3a52b92c1db2c6f25a1a8d
  • Pointer size: 131 Bytes
  • Size of remote file: 337 kB
assets/molforge_architecture.png ADDED

Git LFS Details

  • SHA256: 3674e13e70719039a42f7a35720e7f3f2657c9980530f22e03a63207e18d457a
  • Pointer size: 131 Bytes
  • Size of remote file: 337 kB
assets/reward_curve.png ADDED

Git LFS Details

  • SHA256: 533a0f6951602ff55c1e63b793bd86dac41e6d90f355894f6476d2a7cbc64245
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
index.html ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MolForge | Verifier-Driven RL for Drug Discovery</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&family=Outfit:wght@400;600;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --primary: #6366f1;
13
+ --primary-glow: rgba(99, 102, 241, 0.5);
14
+ --secondary: #8b5cf6;
15
+ --bg: #0f172a;
16
+ --card-bg: rgba(30, 41, 59, 0.7);
17
+ --text: #f8fafc;
18
+ --text-dim: #94a3b8;
19
+ --glass: rgba(255, 255, 255, 0.03);
20
+ --glass-border: rgba(255, 255, 255, 0.1);
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ font-family: 'Inter', sans-serif;
31
+ background-color: var(--bg);
32
+ background-image:
33
+ radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
34
+ radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.15) 0%, transparent 40%);
35
+ color: var(--text);
36
+ line-height: 1.6;
37
+ overflow-x: hidden;
38
+ }
39
+
40
+ .container {
41
+ max-width: 1100px;
42
+ margin: 0 auto;
43
+ padding: 0 2rem;
44
+ }
45
+
46
+ /* Hero Section */
47
+ header {
48
+ height: 90vh;
49
+ display: flex;
50
+ flex-direction: column;
51
+ justify-content: center;
52
+ align-items: center;
53
+ text-align: center;
54
+ position: relative;
55
+ }
56
+
57
+ .badge {
58
+ background: var(--glass);
59
+ border: 1px solid var(--glass-border);
60
+ padding: 0.5rem 1.2rem;
61
+ border-radius: 99px;
62
+ font-size: 0.85rem;
63
+ font-weight: 600;
64
+ color: var(--primary);
65
+ margin-bottom: 1.5rem;
66
+ display: inline-block;
67
+ backdrop-filter: blur(10px);
68
+ }
69
+
70
+ h1 {
71
+ font-family: 'Outfit', sans-serif;
72
+ font-size: clamp(3rem, 8vw, 5.5rem);
73
+ font-weight: 800;
74
+ line-height: 1.1;
75
+ margin-bottom: 1.5rem;
76
+ background: linear-gradient(to bottom right, #fff 30%, var(--text-dim));
77
+ -webkit-background-clip: text;
78
+ -webkit-text-fill-color: transparent;
79
+ }
80
+
81
+ .hero-tagline {
82
+ font-size: clamp(1.1rem, 3vw, 1.4rem);
83
+ color: var(--text-dim);
84
+ max-width: 700px;
85
+ margin-bottom: 3rem;
86
+ }
87
+
88
+ .cta-group {
89
+ display: flex;
90
+ gap: 1.5rem;
91
+ flex-wrap: wrap;
92
+ justify-content: center;
93
+ }
94
+
95
+ .btn {
96
+ padding: 1rem 2.5rem;
97
+ border-radius: 12px;
98
+ font-weight: 700;
99
+ text-decoration: none;
100
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
101
+ display: inline-flex;
102
+ align-items: center;
103
+ gap: 0.5rem;
104
+ }
105
+
106
+ .btn-primary {
107
+ background: var(--primary);
108
+ color: white;
109
+ box-shadow: 0 10px 20px -10px var(--primary-glow);
110
+ }
111
+
112
+ .btn-primary:hover {
113
+ transform: translateY(-2px);
114
+ box-shadow: 0 15px 30px -10px var(--primary-glow);
115
+ filter: brightness(1.1);
116
+ }
117
+
118
+ .btn-secondary {
119
+ background: var(--glass);
120
+ border: 1px solid var(--glass-border);
121
+ color: white;
122
+ }
123
+
124
+ .btn-secondary:hover {
125
+ background: var(--glass-border);
126
+ transform: translateY(-2px);
127
+ }
128
+
129
+ /* Section Styling */
130
+ section {
131
+ padding: 8rem 0;
132
+ }
133
+
134
+ .section-header {
135
+ margin-bottom: 4rem;
136
+ text-align: center;
137
+ }
138
+
139
+ .section-header h2 {
140
+ font-family: 'Outfit', sans-serif;
141
+ font-size: 2.5rem;
142
+ margin-bottom: 1rem;
143
+ }
144
+
145
+ .section-header p {
146
+ color: var(--text-dim);
147
+ max-width: 600px;
148
+ margin: 0 auto;
149
+ }
150
+
151
+ /* Pillars Grid */
152
+ .pillars-grid {
153
+ display: grid;
154
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
155
+ gap: 2rem;
156
+ }
157
+
158
+ .pillar-card {
159
+ background: var(--card-bg);
160
+ border: 1px solid var(--glass-border);
161
+ padding: 2.5rem;
162
+ border-radius: 24px;
163
+ transition: all 0.4s ease;
164
+ backdrop-filter: blur(12px);
165
+ }
166
+
167
+ .pillar-card:hover {
168
+ transform: translateY(-10px);
169
+ border-color: var(--primary);
170
+ box-shadow: 0 20px 40px -20px rgba(0,0,0,0.5);
171
+ }
172
+
173
+ .pillar-icon {
174
+ font-size: 2rem;
175
+ margin-bottom: 1.5rem;
176
+ background: var(--glass);
177
+ width: 60px;
178
+ height: 60px;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ border-radius: 16px;
183
+ }
184
+
185
+ .pillar-card h3 {
186
+ font-size: 1.4rem;
187
+ margin-bottom: 1rem;
188
+ color: var(--primary);
189
+ }
190
+
191
+ .pillar-card p {
192
+ color: var(--text-dim);
193
+ font-size: 0.95rem;
194
+ }
195
+
196
+ /* Visuals Section */
197
+ .visual-container {
198
+ background: var(--card-bg);
199
+ border: 1px solid var(--glass-border);
200
+ border-radius: 32px;
201
+ padding: 3rem;
202
+ margin-bottom: 4rem;
203
+ overflow: hidden;
204
+ }
205
+
206
+ .visual-container img {
207
+ width: 100%;
208
+ height: auto;
209
+ border-radius: 16px;
210
+ box-shadow: 0 20px 50px rgba(0,0,0,0.4);
211
+ }
212
+
213
+ .visual-label {
214
+ display: block;
215
+ text-align: center;
216
+ margin-top: 1.5rem;
217
+ color: var(--text-dim);
218
+ font-weight: 500;
219
+ }
220
+
221
+ /* Results Table */
222
+ .table-wrapper {
223
+ overflow-x: auto;
224
+ background: var(--glass);
225
+ border-radius: 20px;
226
+ border: 1px solid var(--glass-border);
227
+ }
228
+
229
+ table {
230
+ width: 100%;
231
+ border-collapse: collapse;
232
+ text-align: left;
233
+ }
234
+
235
+ th, td {
236
+ padding: 1.5rem;
237
+ border-bottom: 1px solid var(--glass-border);
238
+ }
239
+
240
+ th {
241
+ background: rgba(255,255,255,0.05);
242
+ font-weight: 700;
243
+ text-transform: uppercase;
244
+ font-size: 0.75rem;
245
+ letter-spacing: 0.1em;
246
+ color: var(--text-dim);
247
+ }
248
+
249
+ .improvement {
250
+ color: #10b981;
251
+ font-weight: 800;
252
+ }
253
+
254
+ /* POMDP Info */
255
+ .pomdp-box {
256
+ display: grid;
257
+ grid-template-columns: 1fr 1fr;
258
+ gap: 2rem;
259
+ margin-top: 3rem;
260
+ }
261
+
262
+ .state-card {
263
+ background: var(--glass);
264
+ padding: 2rem;
265
+ border-radius: 20px;
266
+ border-left: 4px solid var(--primary);
267
+ }
268
+
269
+ .state-card h4 {
270
+ margin-bottom: 1rem;
271
+ color: var(--text);
272
+ }
273
+
274
+ /* Footer */
275
+ footer {
276
+ padding: 6rem 0;
277
+ text-align: center;
278
+ border-top: 1px solid var(--glass-border);
279
+ }
280
+
281
+ .footer-links {
282
+ display: flex;
283
+ justify-content: center;
284
+ gap: 2rem;
285
+ margin-bottom: 2rem;
286
+ }
287
+
288
+ .footer-links a {
289
+ color: var(--text-dim);
290
+ text-decoration: none;
291
+ font-weight: 600;
292
+ transition: color 0.3s;
293
+ }
294
+
295
+ .footer-links a:hover {
296
+ color: var(--primary);
297
+ }
298
+
299
+ @media (max-width: 768px) {
300
+ .pomdp-box {
301
+ grid-template-columns: 1fr;
302
+ }
303
+ h1 { font-size: 3rem; }
304
+ .pillars-grid { grid-template-columns: 1fr; }
305
+ }
306
+ </style>
307
+ </head>
308
+ <body>
309
+
310
+ <div class="container">
311
+ <!-- Hero Section -->
312
+ <header>
313
+ <div class="badge">OpenEnv Hackathon 2026</div>
314
+ <h1>MolForge</h1>
315
+ <p class="hero-tagline">A verifier-driven reinforcement learning environment for oncology drug discovery, where the LLM is the scientist, not the judge.</p>
316
+ <div class="cta-group">
317
+ <a href="https://colab.research.google.com/drive/1c6npGkGNbbbd8XFNeS6zInBpopLnJ4W4?usp=sharing" target="_blank" class="btn btn-primary">
318
+ Launch Training Notebook
319
+ </a>
320
+ <a href="#pillars" class="btn btn-secondary">Explore the Pillars</a>
321
+ </div>
322
+ </header>
323
+
324
+ <!-- POMDP Section -->
325
+ <section id="architecture">
326
+ <div class="section-header">
327
+ <h2>Scientific Architecture</h2>
328
+ <p>MolForge operates as a Partially Observable Markov Decision Process (POMDP), forcing models to operate under real-world uncertainty.</p>
329
+ </div>
330
+
331
+ <div class="visual-container">
332
+ <img src="assets/molforge_architecture.png" alt="MolForge Architecture">
333
+ <span class="visual-label">Closed-loop scientific feedback architecture</span>
334
+ </div>
335
+
336
+ <div class="pomdp-box">
337
+ <div class="state-card">
338
+ <h4>Hidden Reality</h4>
339
+ <p>The ground-truth scoring for potency, safety, and synthesizability. Includes late-stage mutation traps that only evidence can reveal.</p>
340
+ </div>
341
+ <div class="state-card">
342
+ <h4>Visible Evidence</h4>
343
+ <p>Noisy assay reports from RDKit and TDC, remaining budget, and structured feedback from the governance board.</p>
344
+ </div>
345
+ </div>
346
+ </section>
347
+
348
+ <!-- Pillars Section -->
349
+ <section id="pillars">
350
+ <div class="section-header">
351
+ <h2>The Seven Pillars</h2>
352
+ <p>Beyond simple molecule generation: a complete medicinal chemistry workflow optimizer.</p>
353
+ </div>
354
+
355
+ <div class="pillars-grid">
356
+ <div class="pillar-card">
357
+ <div class="pillar-icon">🧪</div>
358
+ <h3>Verifier-First</h3>
359
+ <p>The LLM is held accountable by RDKit and TDC simulation engines. It must justify every decision with verifiable data.</p>
360
+ </div>
361
+ <div class="pillar-card">
362
+ <div class="pillar-icon">🧬</div>
363
+ <h3>Physics Grounded</h3>
364
+ <p>Heuristic docking scores simulate pocket matching, lipophilic fit, and polarity clash in milliseconds.</p>
365
+ </div>
366
+ <div class="pillar-card">
367
+ <div class="pillar-icon">🔄</div>
368
+ <h3>Self-Correction</h3>
369
+ <p>A structured loop where agents receive reviews on their edits and iteratively repair candidates.</p>
370
+ </div>
371
+ <div class="pillar-card">
372
+ <div class="pillar-icon">📊</div>
373
+ <h3>Decomposed Rewards</h3>
374
+ <p>Fine-grained observability into research, edits, and coordination—not just a single vague scalar.</p>
375
+ </div>
376
+ <div class="pillar-card">
377
+ <div class="pillar-icon">🔬</div>
378
+ <h3>Evidence-Based</h3>
379
+ <p>Constant, verifiable reviews drive the model toward sound scientific design rather than pattern matching.</p>
380
+ </div>
381
+ <div class="pillar-card">
382
+ <div class="pillar-icon">🎓</div>
383
+ <h3>Curriculum Learning</h3>
384
+ <p>Partial credit "breadcrumbs" for early RL exploration, transitioning to strict evaluation for grading.</p>
385
+ </div>
386
+ <div class="pillar-card">
387
+ <div class="pillar-icon">🤝</div>
388
+ <h3>Governance</h3>
389
+ <p>Multi-agent specialist board reviews every plan and execution to ensure rigor and safety.</p>
390
+ </div>
391
+ </div>
392
+ </section>
393
+
394
+ <!-- Results Section -->
395
+ <section id="results">
396
+ <div class="section-header">
397
+ <h2>Training & Performance</h2>
398
+ <p>Comparing the Supervised Fine-Tuning (SFT) baseline against the final GRPO-trained policy.</p>
399
+ </div>
400
+
401
+ <div class="visual-container">
402
+ <img src="assets/reward_curve.png" alt="Reward Curve">
403
+ <span class="visual-label">Learning progression from sparse rewards to consistent submissions</span>
404
+ </div>
405
+
406
+ <div class="table-wrapper">
407
+ <table>
408
+ <thead>
409
+ <tr>
410
+ <th>Scenario Difficulty</th>
411
+ <th>Before (SFT)</th>
412
+ <th>After (RL)</th>
413
+ <th>Improvement</th>
414
+ </tr>
415
+ </thead>
416
+ <tbody>
417
+ <tr>
418
+ <td><strong>Level 0: Easy</strong></td>
419
+ <td>0.1167</td>
420
+ <td>0.1295</td>
421
+ <td class="improvement">+10.9%</td>
422
+ </tr>
423
+ <tr>
424
+ <td><strong>Level 1: Medium</strong></td>
425
+ <td>0.1167</td>
426
+ <td>0.1278</td>
427
+ <td class="improvement">+9.5%</td>
428
+ </tr>
429
+ <tr>
430
+ <td><strong>Level 2: Hard</strong></td>
431
+ <td>0.0800</td>
432
+ <td>0.0866</td>
433
+ <td class="improvement">+8.3%</td>
434
+ </tr>
435
+ </tbody>
436
+ </table>
437
+ </div>
438
+
439
+ <div style="margin-top: 4rem;">
440
+ <img src="assets/Logs.png" alt="Training Logs" style="width: 100%; border-radius: 20px; border: 1px solid var(--glass-border);">
441
+ <span class="visual-label">Detailed action telemetry and governance history</span>
442
+ </div>
443
+ </section>
444
+
445
+ <!-- Footer -->
446
+ <footer>
447
+ <div class="footer-links">
448
+ <a href="https://github.com/Adhitya-Vardhan/molt_lab" target="_blank">GitHub Repository</a>
449
+ <a href="https://huggingface.co/Adhitya122/molforge-grpo-oncology" target="_blank">Model Card</a>
450
+ <a href="https://colab.research.google.com/drive/1c6npGkGNbbbd8XFNeS6zInBpopLnJ4W4?usp=sharing" target="_blank">Colab Notebook</a>
451
+ </div>
452
+ <p style="color: var(--text-dim); font-size: 0.9rem;">Built for the OpenEnv Hackathon 2026</p>
453
+ </footer>
454
+ </div>
455
+
456
+ </body>
457
+ </html>
molforge_grpo_official_submission.ipynb CHANGED
@@ -54,7 +54,7 @@
54
  "os.environ[\"MOLFORGE_REWARD_MODE\"] = \"curriculum\"\n",
55
  "os.environ[\"MOLFORGE_TRAINING_RANDOMIZATION\"] = \"1\"\n",
56
  "\n",
57
- "RL_MAX_STEPS = 80\n",
58
  "NUM_GENERATIONS = 2\n",
59
  "PER_DEVICE_BATCH = 2\n",
60
  "GRAD_ACCUM = 4\n",
@@ -69,7 +69,9 @@
69
  "PLOT_DIR = OUTPUT_DIR / \"plots\"\n",
70
  "\n",
71
  "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
72
- "PLOT_DIR.mkdir(parents=True, exist_ok=True)"
 
 
73
  ]
74
  },
75
  {
@@ -88,70 +90,77 @@
88
  "outputs": [],
89
  "source": [
90
  "import json\n",
 
91
  "from typing import Any, Dict, Tuple\n",
92
- "from inference_common import (\n",
93
- " MolForgeAction,\n",
94
- " attach_reasoning_fields,\n",
95
- " attach_team_messages,\n",
96
- " extract_json,\n",
97
- ")\n",
98
  "from server.molforge_environment import MolForgeEnvironment\n",
99
- "from models import MolForgeState\n",
 
100
  "\n",
101
  "def replay_to_state(record: dict[str, Any]) -> MolForgeEnvironment:\n",
102
  " env = MolForgeEnvironment()\n",
103
- " env._state = MolForgeState(**record[\"state\"])\n",
104
- " env._molecule = dict(record[\"molecule\"])\n",
105
- " env._scenario = [s for s in env.SCENARIOS if s.scenario_id == env._state.scenario_id][0]\n",
 
 
 
106
  " return env\n",
107
  "\n",
108
- "def evaluate_completion(prompt_str: str, completion_str: str, record: dict[str, Any]) -> Tuple[float, dict]:\n",
109
- " diagnostics = {\"valid_json\": False}\n",
110
  " try:\n",
111
  " action_dict = extract_json(completion_str)\n",
112
  " action = MolForgeAction(**action_dict)\n",
 
113
  " except Exception:\n",
114
- " return -1.2, diagnostics\n",
115
  "\n",
116
- " diagnostics[\"valid_json\"] = True\n",
117
  " env = replay_to_state(record)\n",
118
- " \n",
119
- " # Create empty observation and attach reasoning\n",
120
  " observation = env._build_observation(reward=0.0, done=False, reward_components=[])\n",
121
  " action = attach_team_messages(observation, attach_reasoning_fields(observation, action))\n",
122
- " \n",
123
- " # Step the OpenEnv environment\n",
124
  " next_observation = env.step(action)\n",
125
- " reward = float(next_observation.reward)\n",
126
- " grader_scores = next_observation.metadata.get(\"terminal_grader_scores\", {})\n",
127
  " \n",
128
- " # --- ANTI-REWARD-HACKING SHAPING ---\n",
129
- " if action.action_type == \"run_assay\" and reward > 0:\n",
130
- " reward *= 0.25 # Nerf assay farming\n",
131
- " elif action.action_type == \"submit\":\n",
132
- " sub_score = float(grader_scores.get(\"submission_score\", 0.0))\n",
133
- " if sub_score > 0.0:\n",
134
- " reward += sub_score * 3.0 # Massive multiplier for submissions\n",
135
- " elif action.action_type == \"edit\" and reward > 0:\n",
136
- " reward *= 1.5 # Boost edits\n",
 
 
 
 
 
137
  "\n",
138
- " diagnostics.update({\n",
139
- " \"action_type\": action.action_type,\n",
140
- " \"reward\": reward,\n",
141
- " \"done\": next_observation.done,\n",
142
- " })\n",
143
- " return reward, diagnostics\n",
 
 
 
 
 
 
 
 
 
 
144
  "\n",
145
  "def molforge_reward_func(prompts, completions, **kwargs) -> list[float]:\n",
146
  " rewards = []\n",
147
- " dataset_records = kwargs.get(\"record\", [])\n",
148
- " \n",
149
- " for prompt_list, completion, record in zip(prompts, completions, dataset_records):\n",
150
- " prompt_str = prompt_list[-1][\"content\"] if isinstance(prompt_list, list) else str(prompt_list)\n",
151
- " completion_str = completion[0][\"content\"] if isinstance(completion, list) else str(completion)\n",
152
- " reward, _ = evaluate_completion(prompt_str, completion_str, record)\n",
153
  " rewards.append(reward)\n",
154
- " return rewards"
 
 
155
  ]
156
  },
157
  {
@@ -169,9 +178,9 @@
169
  "source": [
170
  "from unsloth import FastLanguageModel\n",
171
  "\n",
172
- "# Set this to your SFT checkpoint\n",
173
- "# You can set this to a local path or a Hugging Face repo\n",
174
- "SFT_ADAPTER_PATH = \"/content/drive/MyDrive/Qwen_3.5_finetune/qwen3_5_2b_lora_adapters_compact_v4\" # <-- Change to your path\n",
175
  "\n",
176
  "print(\"Loading model and applying Unsloth optimizations...\")\n",
177
  "model, tokenizer = FastLanguageModel.from_pretrained(\n",
@@ -202,47 +211,91 @@
202
  "metadata": {},
203
  "outputs": [],
204
  "source": [
205
- "from trl import GRPOConfig, GRPOTrainer\n",
206
  "from datasets import Dataset\n",
207
  "from scripts.generate_sft_compact_policy_v4_dataset import compact_action_payload, COMPACT_ACTION_SYSTEM_PROMPT\n",
 
 
208
  "\n",
209
- "# Load dataset\n",
210
- "def load_prompt_dataset() -> Dataset:\n",
211
- " import json\n",
212
- " data = []\n",
213
- " with open(\"data/molforge_sft_compact_policy_v4.jsonl\", \"r\") as f:\n",
214
- " for line in f:\n",
215
- " record = json.loads(line)\n",
216
- " prompt_text = compact_action_payload(record)\n",
217
- " data.append({\n",
 
 
 
 
 
 
 
 
218
  " \"prompt\": [\n",
219
  " {\"role\": \"system\", \"content\": COMPACT_ACTION_SYSTEM_PROMPT},\n",
220
- " {\"role\": \"user\", \"content\": prompt_text}\n",
221
  " ],\n",
222
- " \"record\": record\n",
 
 
 
 
 
 
 
223
  " })\n",
224
- " return Dataset.from_list(data)\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  "\n",
226
- "dataset = load_prompt_dataset()\n",
 
 
227
  "\n",
228
- "# Configure GRPO\n",
229
- "training_args = GRPOConfig(\n",
230
- " output_dir=str(OUTPUT_DIR),\n",
231
- " learning_rate=LEARNING_RATE,\n",
232
- " per_device_train_batch_size=PER_DEVICE_BATCH,\n",
233
- " gradient_accumulation_steps=GRAD_ACCUM,\n",
234
- " max_prompt_length=MAX_PROMPT_LENGTH,\n",
235
- " max_completion_length=MAX_COMPLETION_LENGTH,\n",
236
- " num_generations=NUM_GENERATIONS,\n",
237
- " max_steps=RL_MAX_STEPS,\n",
238
- " logging_steps=1,\n",
239
- " save_steps=25,\n",
240
- " bf16=True,\n",
241
- " report_to=\"none\",\n",
242
- " log_completions=True,\n",
243
- ")\n",
 
 
 
 
 
244
  "\n",
245
- "# Initialize Trainer\n",
246
  "trainer = GRPOTrainer(\n",
247
  " model=model,\n",
248
  " reward_funcs=molforge_reward_func,\n",
@@ -256,7 +309,7 @@
256
  "\n",
257
  "print(f\"Training complete. Saving adapters to {ADAPTER_SAVE_DIR}\")\n",
258
  "trainer.save_model(str(ADAPTER_SAVE_DIR))\n",
259
- "tokenizer.save_pretrained(str(ADAPTER_SAVE_DIR))"
260
  ]
261
  }
262
  ],
 
54
  "os.environ[\"MOLFORGE_REWARD_MODE\"] = \"curriculum\"\n",
55
  "os.environ[\"MOLFORGE_TRAINING_RANDOMIZATION\"] = \"1\"\n",
56
  "\n",
57
+ "RL_MAX_STEPS = 300\n",
58
  "NUM_GENERATIONS = 2\n",
59
  "PER_DEVICE_BATCH = 2\n",
60
  "GRAD_ACCUM = 4\n",
 
69
  "PLOT_DIR = OUTPUT_DIR / \"plots\"\n",
70
  "\n",
71
  "OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
72
+ "PLOT_DIR.mkdir(parents=True, exist_ok=True)\n",
73
+ "LOG_DIR = OUTPUT_DIR / \"logs\"\n",
74
+ "LOG_DIR.mkdir(parents=True, exist_ok=True)\n"
75
  ]
76
  },
77
  {
 
90
  "outputs": [],
91
  "source": [
92
  "import json\n",
93
+ "import time\n",
94
  "from typing import Any, Dict, Tuple\n",
95
+ "from inference_common import MolForgeAction, attach_reasoning_fields, attach_team_messages, extract_json\n",
 
 
 
 
 
96
  "from server.molforge_environment import MolForgeEnvironment\n",
97
+ "\n",
98
+ "COMPLETION_LOG = LOG_DIR / \"completion_diagnostics.jsonl\"\n",
99
  "\n",
100
  "def replay_to_state(record: dict[str, Any]) -> MolForgeEnvironment:\n",
101
  " env = MolForgeEnvironment()\n",
102
+ " if record.get(\"randomized\"): os.environ[\"MOLFORGE_TRAINING_RANDOMIZATION\"] = \"1\"\n",
103
+ " os.environ[\"MOLFORGE_RAND_SEED\"] = str(record.get(\"random_seed\", \"rl\"))\n",
104
+ " observation = env.reset()\n",
105
+ " for action_payload in record.get(\"pre_actions\", []):\n",
106
+ " action = MolForgeAction(**action_payload)\n",
107
+ " observation = env.step(attach_team_messages(observation, attach_reasoning_fields(observation, action)))\n",
108
  " return env\n",
109
  "\n",
110
+ "def evaluate_completion(prompt_str, completion_str, record) -> Tuple[float, dict]:\n",
 
111
  " try:\n",
112
  " action_dict = extract_json(completion_str)\n",
113
  " action = MolForgeAction(**action_dict)\n",
114
+ " valid_json = True\n",
115
  " except Exception:\n",
116
+ " return -1.5, {\"valid_json\": False, \"action_type\": \"invalid\"}\n",
117
  "\n",
 
118
  " env = replay_to_state(record)\n",
 
 
119
  " observation = env._build_observation(reward=0.0, done=False, reward_components=[])\n",
120
  " action = attach_team_messages(observation, attach_reasoning_fields(observation, action))\n",
 
 
121
  " next_observation = env.step(action)\n",
 
 
122
  " \n",
123
+ " # --- ANTI-REWARD HACKING FILTER ---\n",
124
+ " # We manually sum only the scientific reward components, ignoring \"chatter\" rewards\n",
125
+ " filtered_reward = 0.0\n",
126
+ " keep_components = {\n",
127
+ " \"edit_delta\", \"submission_quality\", \"hard_constraints\", \"baseline_gate\",\n",
128
+ " \"submission_evidence\", \"curriculum_terminal_progress\", \"curriculum_evidence_gate\"\n",
129
+ " }\n",
130
+ " penalties = {\"invalid_action\", \"budget_exhausted\", \"step_limit\", \"policy_veto\", \"loop_penalty\"}\n",
131
+ " \n",
132
+ " for component in next_observation.reward_components:\n",
133
+ " if component.name in keep_components:\n",
134
+ " filtered_reward += component.value\n",
135
+ " elif component.name in penalties:\n",
136
+ " filtered_reward += component.value\n",
137
  "\n",
138
+ " # Add a mandatory time pressure penalty for every step\n",
139
+ " filtered_reward -= 0.15 \n",
140
+ " \n",
141
+ " grader_scores = next_observation.metadata.get(\"terminal_grader_scores\", {})\n",
142
+ " \n",
143
+ " # Extra multipliers for reaching the goal\n",
144
+ " if action.action_type == \"submit\" and grader_scores.get(\"submission_score\", 0) > 0:\n",
145
+ " filtered_reward += float(grader_scores[\"submission_score\"]) * 4.0\n",
146
+ " \n",
147
+ " reward = round(filtered_reward, 4)\n",
148
+ " \n",
149
+ " return reward, {\n",
150
+ " \"valid_json\": True, \"action_type\": action.action_type, \"reward\": reward, \n",
151
+ " \"done\": next_observation.done, \"scores\": grader_scores, \n",
152
+ " \"raw_completion\": completion_str, \"timestamp\": time.time()\n",
153
+ " }\n",
154
  "\n",
155
  "def molforge_reward_func(prompts, completions, **kwargs) -> list[float]:\n",
156
  " rewards = []\n",
157
+ " for i in range(len(completions)):\n",
158
+ " record = {\"pre_actions\": kwargs[\"record\"][i][\"pre_actions\"] if \"record\" in kwargs else []}\n",
159
+ " reward, diagnostics = evaluate_completion(\"\", completions[i][0][\"content\"], record)\n",
 
 
 
160
  " rewards.append(reward)\n",
161
+ " with open(COMPLETION_LOG, \"a\") as f:\n",
162
+ " f.write(json.dumps(diagnostics) + \"\\n\")\n",
163
+ " return rewards\n"
164
  ]
165
  },
166
  {
 
178
  "source": [
179
  "from unsloth import FastLanguageModel\n",
180
  "\n",
181
+ "# Set this to your SFT checkpoint Deployed to hugging face space \n",
182
+ "# SFT trained on only to mimic the response behavioiur of the model (structured responses visit the hf blog for more detailed explanation )\n",
183
+ "SFT_ADAPTER_PATH = \"Adhitya122/qwen3_5_2b_molforge_sft_v4\"\n",
184
  "\n",
185
  "print(\"Loading model and applying Unsloth optimizations...\")\n",
186
  "model, tokenizer = FastLanguageModel.from_pretrained(\n",
 
211
  "metadata": {},
212
  "outputs": [],
213
  "source": [
 
214
  "from datasets import Dataset\n",
215
  "from scripts.generate_sft_compact_policy_v4_dataset import compact_action_payload, COMPACT_ACTION_SYSTEM_PROMPT\n",
216
+ "from inference_common import heuristic_team_action\n",
217
+ "import random\n",
218
  "\n",
219
+ "def build_dynamic_prompts(episodes=50, max_turns=5) -> Dataset:\n",
220
+ " \"\"\"Generates training prompts by playing the environment with a heuristic expert.\"\"\"\n",
221
+ " print(f\"Generating {episodes} episodes of dynamic prompts...\")\n",
222
+ " records = []\n",
223
+ " env = MolForgeEnvironment()\n",
224
+ " \n",
225
+ " for _ in range(episodes):\n",
226
+ " observation = env.reset()\n",
227
+ " pre_actions = []\n",
228
+ " \n",
229
+ " for _ in range(max_turns):\n",
230
+ " if observation.done:\n",
231
+ " break\n",
232
+ " \n",
233
+ " # Capture the current state as a prompt\n",
234
+ " prompt_payload = compact_action_payload(observation)\n",
235
+ " records.append({\n",
236
  " \"prompt\": [\n",
237
  " {\"role\": \"system\", \"content\": COMPACT_ACTION_SYSTEM_PROMPT},\n",
238
+ " {\"role\": \"user\", \"content\": json.dumps(prompt_payload)}\n",
239
  " ],\n",
240
+ " \"record\": {\n",
241
+ " \"scenario_id\": observation.scenario_id,\n",
242
+ " \"difficulty\": observation.difficulty,\n",
243
+ " \"step_index\": observation.step_index,\n",
244
+ " \"pre_actions\": list(pre_actions),\n",
245
+ " \"randomized\": True,\n",
246
+ " \"random_seed\": \"dynamic-rl\"\n",
247
+ " }\n",
248
  " })\n",
249
+ " \n",
250
+ " # Use expert to move to the next state\n",
251
+ " action = heuristic_team_action(observation)\n",
252
+ " observation = env.step(action)\n",
253
+ " pre_actions.append({\"action_type\": action.action_type, \"acting_role\": action.acting_role})\n",
254
+ " \n",
255
+ " random.shuffle(records)\n",
256
+ " return Dataset.from_list(records)\n",
257
+ "\n",
258
+ "# Generate the dataset dynamically (no .jsonl needed!)\n",
259
+ "dataset = build_dynamic_prompts(episodes=20, max_turns=6)\n",
260
+ "print(f\"Dynamic dataset created with {len(dataset)} prompt states.\")\n"
261
+ ]
262
+ },
263
+ {
264
+ "cell_type": "code",
265
+ "execution_count": null,
266
+ "metadata": {},
267
+ "outputs": [],
268
+ "source": [
269
+ "from trl import GRPOConfig, GRPOTrainer\n",
270
+ "import inspect\n",
271
+ "import torch\n",
272
  "\n",
273
+ "# Check for BF16 support (T4 does not support it, A100/L4 do)\n",
274
+ "has_bf16 = torch.cuda.is_bf16_supported()\n",
275
+ "print(f\"GPU supports BF16: {has_bf16}\")\n",
276
  "\n",
277
+ "config_kwargs = {\n",
278
+ " \"output_dir\": str(OUTPUT_DIR),\n",
279
+ " \"learning_rate\": LEARNING_RATE,\n",
280
+ " \"per_device_train_batch_size\": PER_DEVICE_BATCH,\n",
281
+ " \"gradient_accumulation_steps\": GRAD_ACCUM,\n",
282
+ " \"max_prompt_length\": MAX_PROMPT_LENGTH,\n",
283
+ " \"max_completion_length\": MAX_COMPLETION_LENGTH,\n",
284
+ " \"num_generations\": NUM_GENERATIONS,\n",
285
+ " \"max_steps\": RL_MAX_STEPS,\n",
286
+ " \"logging_steps\": 1,\n",
287
+ " \"save_steps\": 25,\n",
288
+ " \"bf16\": has_bf16,\n",
289
+ " \"fp16\": not has_bf16,\n",
290
+ " \"report_to\": \"none\",\n",
291
+ " \"log_completions\": True,\n",
292
+ "}\n",
293
+ "\n",
294
+ "supported_params = inspect.signature(GRPOConfig.__init__).parameters\n",
295
+ "filtered_kwargs = {k: v for k, v in config_kwargs.items() if k in supported_params}\n",
296
+ "\n",
297
+ "training_args = GRPOConfig(**filtered_kwargs)\n",
298
  "\n",
 
299
  "trainer = GRPOTrainer(\n",
300
  " model=model,\n",
301
  " reward_funcs=molforge_reward_func,\n",
 
309
  "\n",
310
  "print(f\"Training complete. Saving adapters to {ADAPTER_SAVE_DIR}\")\n",
311
  "trainer.save_model(str(ADAPTER_SAVE_DIR))\n",
312
+ "tokenizer.save_pretrained(str(ADAPTER_SAVE_DIR))\n"
313
  ]
314
  }
315
  ],