Spaces:
Sleeping
Sleeping
Commit ·
1edadfb
1
Parent(s): 8c686ce
discovery plot
Browse files- .gitignore +0 -1
- Alien_Physics_Discovery_OpenEnv.ipynb +1 -787
- artifacts/{discovery_plot.png → results/discovery_plot.png} +2 -2
- artifacts/{grader_report.json → results/grader_report.json} +0 -0
- artifacts/{reward_curve.png → results/reward_curve.png} +0 -0
- baselines.py +3 -3
- scripts/visualize_discovery.py +2 -2
- theory_dsl.py +4 -0
.gitignore
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
| 4 |
.pytest_cache/
|
| 5 |
-
# artifacts/
|
| 6 |
*.egg-info/
|
| 7 |
test_results
|
| 8 |
.claude/
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
| 4 |
.pytest_cache/
|
|
|
|
| 5 |
*.egg-info/
|
| 6 |
test_results
|
| 7 |
.claude/
|
Alien_Physics_Discovery_OpenEnv.ipynb
CHANGED
|
@@ -1,787 +1 @@
|
|
| 1 |
-
{
|
| 2 |
-
"nbformat": 4,
|
| 3 |
-
"nbformat_minor": 5,
|
| 4 |
-
"metadata": {
|
| 5 |
-
"kernelspec": {
|
| 6 |
-
"display_name": "Python 3",
|
| 7 |
-
"language": "python",
|
| 8 |
-
"name": "python3"
|
| 9 |
-
},
|
| 10 |
-
"language_info": {
|
| 11 |
-
"name": "python",
|
| 12 |
-
"version": "3.10.0"
|
| 13 |
-
},
|
| 14 |
-
"colab": {
|
| 15 |
-
"provenance": [],
|
| 16 |
-
"gpuType": "T4"
|
| 17 |
-
},
|
| 18 |
-
"accelerator": "GPU"
|
| 19 |
-
},
|
| 20 |
-
"cells": [
|
| 21 |
-
{
|
| 22 |
-
"cell_type": "markdown",
|
| 23 |
-
"id": "md-title",
|
| 24 |
-
"metadata": {},
|
| 25 |
-
"source": [
|
| 26 |
-
"# Alien Physics Discovery \u2014 OpenEnv Hackathon 2026\n",
|
| 27 |
-
"### Train an LLM to do unsupervised scientific discovery from raw sensor data\n",
|
| 28 |
-
"\n",
|
| 29 |
-
"**Theme:** Wild Card (Theme 5) + World Modeling (Theme 3.1) + Self-Improvement (Theme 4)\n",
|
| 30 |
-
"\n",
|
| 31 |
-
"**The Problem:** LLMs can write code and explain physics, but have never been trained to\n",
|
| 32 |
-
"*discover* physical structure from raw, ambiguous time-series data through iterative theory revision.\n",
|
| 33 |
-
"\n",
|
| 34 |
-
"**The Environment:** An agent observes noisy sensor logs from a simulated alien universe\n",
|
| 35 |
-
"governed by hidden ODEs. It must write a Python `Theory` class that models the dynamics.\n",
|
| 36 |
-
"The environment executes the code against future observations. Reward = log-likelihood \u2212 MDL penalty.\n",
|
| 37 |
-
"\n",
|
| 38 |
-
"---\n",
|
| 39 |
-
"> \u26a1 **Runtime:** T4 GPU (free Colab tier works) \n",
|
| 40 |
-
"> \ud83d\udce6 **Stack:** Unsloth + HF TRL (GRPO) + project's sandboxed execution"
|
| 41 |
-
]
|
| 42 |
-
},
|
| 43 |
-
{
|
| 44 |
-
"cell_type": "markdown",
|
| 45 |
-
"id": "md-s1",
|
| 46 |
-
"metadata": {},
|
| 47 |
-
"source": [
|
| 48 |
-
"## Step 1 \u2014 Install Dependencies"
|
| 49 |
-
]
|
| 50 |
-
},
|
| 51 |
-
{
|
| 52 |
-
"cell_type": "code",
|
| 53 |
-
"execution_count": null,
|
| 54 |
-
"id": "install",
|
| 55 |
-
"metadata": {},
|
| 56 |
-
"outputs": [],
|
| 57 |
-
"source": [
|
| 58 |
-
"%%capture\n",
|
| 59 |
-
"# Unsloth must come first \u2014 it patches transformers in-place\n",
|
| 60 |
-
"!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\"\n",
|
| 61 |
-
"!pip install \"trl>=0.9.0\" peft accelerate bitsandbytes datasets wandb huggingface_hub\n",
|
| 62 |
-
"!pip install matplotlib numpy scipy\n",
|
| 63 |
-
"\n",
|
| 64 |
-
"# Install the Ontology Architect environment from the repo\n",
|
| 65 |
-
"# Replace with your actual repo URL before submitting\n",
|
| 66 |
-
"!pip install \"git+https://github.com/rohan-27p/ontology_architect.git\"\n",
|
| 67 |
-
"\n",
|
| 68 |
-
"print('All dependencies installed')"
|
| 69 |
-
]
|
| 70 |
-
},
|
| 71 |
-
{
|
| 72 |
-
"cell_type": "markdown",
|
| 73 |
-
"id": "md-s2",
|
| 74 |
-
"metadata": {},
|
| 75 |
-
"source": [
|
| 76 |
-
"## Step 2 \u2014 Imports & Config"
|
| 77 |
-
]
|
| 78 |
-
},
|
| 79 |
-
{
|
| 80 |
-
"cell_type": "code",
|
| 81 |
-
"execution_count": null,
|
| 82 |
-
"id": "imports",
|
| 83 |
-
"metadata": {},
|
| 84 |
-
"outputs": [],
|
| 85 |
-
"source": [
|
| 86 |
-
"import json, os, re, warnings\n",
|
| 87 |
-
"from pathlib import Path\n",
|
| 88 |
-
"\n",
|
| 89 |
-
"import numpy as np\n",
|
| 90 |
-
"import torch\n",
|
| 91 |
-
"from datasets import Dataset\n",
|
| 92 |
-
"from unsloth import FastLanguageModel\n",
|
| 93 |
-
"from trl import GRPOConfig, GRPOTrainer\n",
|
| 94 |
-
"import matplotlib.pyplot as plt\n",
|
| 95 |
-
"\n",
|
| 96 |
-
"warnings.filterwarnings('ignore')\n",
|
| 97 |
-
"\n",
|
| 98 |
-
"# \u2500\u2500 Project imports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 99 |
-
"from ontology_architect.baselines import get_baseline\n",
|
| 100 |
-
"from ontology_architect.config import (\n",
|
| 101 |
-
" ExperimentConfig, FeedbackConfig, RewardConfig, SandboxConfig, UniverseConfig\n",
|
| 102 |
-
")\n",
|
| 103 |
-
"from ontology_architect.models import OntologyArchitectAction\n",
|
| 104 |
-
"from ontology_architect.sandbox import TheorySandbox\n",
|
| 105 |
-
"from ontology_architect.reward import score_theory\n",
|
| 106 |
-
"from ontology_architect.server.ontology_architect_environment import OntologyArchitectEnvironment\n",
|
| 107 |
-
"from ontology_architect.universe import ProceduralAlienUniverse\n",
|
| 108 |
-
"\n",
|
| 109 |
-
"# \u2500\u2500 Reproducibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 110 |
-
"SEED = 42\n",
|
| 111 |
-
"np.random.seed(SEED)\n",
|
| 112 |
-
"torch.manual_seed(SEED)\n",
|
| 113 |
-
"\n",
|
| 114 |
-
"# \u2500\u2500 Model \u2014 Qwen2.5-Coder-1.5B fits T4 (16 GB) comfortably \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 115 |
-
"MODEL_NAME = 'unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit'\n",
|
| 116 |
-
"MAX_SEQ_LEN = 2048\n",
|
| 117 |
-
"\n",
|
| 118 |
-
"# \u2500\u2500 Training knobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 119 |
-
"NUM_EPISODES = 60 # oracle curriculum episodes\n",
|
| 120 |
-
"TRAIN_EPOCHS = 2\n",
|
| 121 |
-
"NUM_GENS = 2 # GRPO group size \u2014 2 keeps T4 safe\n",
|
| 122 |
-
"MAX_COMPLETION = 512 # theory code tokens\n",
|
| 123 |
-
"BATCH_SIZE = 1\n",
|
| 124 |
-
"GRAD_ACCUM = 8\n",
|
| 125 |
-
"LR = 2e-5\n",
|
| 126 |
-
"USE_WANDB = False\n",
|
| 127 |
-
"HF_TOKEN = '' # set to push model to Hub\n",
|
| 128 |
-
"\n",
|
| 129 |
-
"# \u2500\u2500 Environment config \u2014 matches tiny_smoke but longer for richer signal \u2500\u2500\n",
|
| 130 |
-
"ENV_CONFIG = ExperimentConfig(\n",
|
| 131 |
-
" universe=UniverseConfig(\n",
|
| 132 |
-
" seed=SEED,\n",
|
| 133 |
-
" family='dual_fluid',\n",
|
| 134 |
-
" observation_window=12,\n",
|
| 135 |
-
" future_window=4,\n",
|
| 136 |
-
" max_steps=4,\n",
|
| 137 |
-
" anomaly_rate=0.06,\n",
|
| 138 |
-
" drift_interval=6,\n",
|
| 139 |
-
" ),\n",
|
| 140 |
-
" sandbox=SandboxConfig(mode='subprocess', timeout_seconds=4.0),\n",
|
| 141 |
-
" reward=RewardConfig(\n",
|
| 142 |
-
" mdl_lambda=0.0005,\n",
|
| 143 |
-
" prediction_sigma=0.12,\n",
|
| 144 |
-
" anomaly_bonus=2.0,\n",
|
| 145 |
-
" drift_bonus=1.0,\n",
|
| 146 |
-
" execution_error_penalty=-25.0,\n",
|
| 147 |
-
" ),\n",
|
| 148 |
-
" feedback=FeedbackConfig(peer_review_window=4, lineage_window=4),\n",
|
| 149 |
-
")\n",
|
| 150 |
-
"\n",
|
| 151 |
-
"print(f'Config ready')\n",
|
| 152 |
-
"print(f' Model : {MODEL_NAME}')\n",
|
| 153 |
-
"print(f' Device : {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"}')\n",
|
| 154 |
-
"print(f' VRAM : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB' if torch.cuda.is_available() else '')"
|
| 155 |
-
]
|
| 156 |
-
},
|
| 157 |
-
{
|
| 158 |
-
"cell_type": "markdown",
|
| 159 |
-
"id": "md-s3",
|
| 160 |
-
"metadata": {},
|
| 161 |
-
"source": [
|
| 162 |
-
"## Step 3 \u2014 Generate Oracle Curriculum\n\nThe teacher baseline rolls out `NUM_EPISODES` episodes. Each step saves `(prompt, completion, reward, future_records)`. The `future_records` are the ground-truth sensor windows \u2014 we store them so the GRPO reward function can score any new theory against the exact same future without re-running the environment."
|
| 163 |
-
]
|
| 164 |
-
},
|
| 165 |
-
{
|
| 166 |
-
"cell_type": "code",
|
| 167 |
-
"execution_count": null,
|
| 168 |
-
"id": "curriculum",
|
| 169 |
-
"metadata": {},
|
| 170 |
-
"outputs": [],
|
| 171 |
-
"source": [
|
| 172 |
-
"CURRICULUM_PATH = Path('artifacts/curriculum/grpo_curriculum.jsonl')\n",
|
| 173 |
-
"CURRICULUM_PATH.parent.mkdir(parents=True, exist_ok=True)\n",
|
| 174 |
-
"\n",
|
| 175 |
-
"FAMILIES = ['dual_fluid', 'coupled_oscillator']\n",
|
| 176 |
-
"teacher = get_baseline('teacher')\n",
|
| 177 |
-
"sandbox = TheorySandbox(ENV_CONFIG.sandbox)\n",
|
| 178 |
-
"\n",
|
| 179 |
-
"examples = []\n",
|
| 180 |
-
"\n",
|
| 181 |
-
"with CURRICULUM_PATH.open('w') as fh:\n",
|
| 182 |
-
" for ep in range(NUM_EPISODES):\n",
|
| 183 |
-
" # Rotate through families so the model sees both universes\n",
|
| 184 |
-
" family = FAMILIES[ep % len(FAMILIES)]\n",
|
| 185 |
-
" cfg = ExperimentConfig(\n",
|
| 186 |
-
" universe=UniverseConfig(\n",
|
| 187 |
-
" seed=SEED + ep * 997,\n",
|
| 188 |
-
" family=family,\n",
|
| 189 |
-
" observation_window=ENV_CONFIG.universe.observation_window,\n",
|
| 190 |
-
" future_window=ENV_CONFIG.universe.future_window,\n",
|
| 191 |
-
" max_steps=ENV_CONFIG.universe.max_steps,\n",
|
| 192 |
-
" anomaly_rate=ENV_CONFIG.universe.anomaly_rate,\n",
|
| 193 |
-
" drift_interval=ENV_CONFIG.universe.drift_interval,\n",
|
| 194 |
-
" ),\n",
|
| 195 |
-
" sandbox=ENV_CONFIG.sandbox,\n",
|
| 196 |
-
" reward=ENV_CONFIG.reward,\n",
|
| 197 |
-
" feedback=ENV_CONFIG.feedback,\n",
|
| 198 |
-
" )\n",
|
| 199 |
-
" env = OntologyArchitectEnvironment(cfg)\n",
|
| 200 |
-
" obs = env.reset()\n",
|
| 201 |
-
"\n",
|
| 202 |
-
" for step in range(cfg.universe.max_steps):\n",
|
| 203 |
-
" # Capture the future window that this step will be scored against\n",
|
| 204 |
-
" cursor = env._cursor\n",
|
| 205 |
-
" future_records = env._records[cursor : cursor + cfg.universe.future_window]\n",
|
| 206 |
-
" sensor_names = list(env._universe.sensor_names)\n",
|
| 207 |
-
"\n",
|
| 208 |
-
" row = {\n",
|
| 209 |
-
" 'prompt' : obs.text,\n",
|
| 210 |
-
" 'completion' : teacher,\n",
|
| 211 |
-
" 'episode' : ep,\n",
|
| 212 |
-
" 'step' : step,\n",
|
| 213 |
-
" 'family' : family,\n",
|
| 214 |
-
" 'sensor_names' : sensor_names,\n",
|
| 215 |
-
" # Serialise future so reward_fn can use it without re-running env\n",
|
| 216 |
-
" 'future_json' : json.dumps([\n",
|
| 217 |
-
" {'t': r.t, 'sensors': r.sensors, 'anomaly': r.anomaly, 'drift': r.drift}\n",
|
| 218 |
-
" for r in future_records\n",
|
| 219 |
-
" ]),\n",
|
| 220 |
-
" }\n",
|
| 221 |
-
"\n",
|
| 222 |
-
" obs = env.step(OntologyArchitectAction(\n",
|
| 223 |
-
" theory_module=teacher,\n",
|
| 224 |
-
" revision_note=f'curriculum ep={ep} step={step}',\n",
|
| 225 |
-
" ))\n",
|
| 226 |
-
" row['reward'] = obs.reward\n",
|
| 227 |
-
" row['metrics'] = json.dumps(obs.metadata.get('last_metrics', {}))\n",
|
| 228 |
-
"\n",
|
| 229 |
-
" fh.write(json.dumps(row) + '\\n')\n",
|
| 230 |
-
" examples.append(row)\n",
|
| 231 |
-
"\n",
|
| 232 |
-
" if obs.done:\n",
|
| 233 |
-
" break\n",
|
| 234 |
-
"\n",
|
| 235 |
-
"print(f'Generated {len(examples)} curriculum examples across {NUM_EPISODES} episodes')\n",
|
| 236 |
-
"print(f' Families: {set(e[\"family\"] for e in examples)}')\n",
|
| 237 |
-
"print(f' Reward range: [{min(e[\"reward\"] for e in examples):.3f}, {max(e[\"reward\"] for e in examples):.3f}]')\n",
|
| 238 |
-
"teacher_mean = float(np.mean([e['reward'] for e in examples]))\n",
|
| 239 |
-
"print(f' Teacher mean reward: {teacher_mean:.4f} (this is the bar GRPO must beat)')"
|
| 240 |
-
]
|
| 241 |
-
},
|
| 242 |
-
{
|
| 243 |
-
"cell_type": "markdown",
|
| 244 |
-
"id": "md-s4",
|
| 245 |
-
"metadata": {},
|
| 246 |
-
"source": [
|
| 247 |
-
"## Step 4 \u2014 Build HuggingFace Dataset"
|
| 248 |
-
]
|
| 249 |
-
},
|
| 250 |
-
{
|
| 251 |
-
"cell_type": "code",
|
| 252 |
-
"execution_count": null,
|
| 253 |
-
"id": "dataset",
|
| 254 |
-
"metadata": {},
|
| 255 |
-
"outputs": [],
|
| 256 |
-
"source": [
|
| 257 |
-
"SYSTEM_PROMPT = \"\"\"You are a scientific discovery agent. An alien universe is governed by hidden physical laws.\n",
|
| 258 |
-
"You observe noisy sensor logs and must write a compact Python Theory class that predicts future readings.\n",
|
| 259 |
-
"\n",
|
| 260 |
-
"Your class MUST implement:\n",
|
| 261 |
-
" fit(history) \u2014 history is a list of dicts: [{'sensors': {'sigma': float, ...}}, ...]\n",
|
| 262 |
-
" predict(window) \u2014 window is {'steps': int, 'history': [...], 'sensor_names': [...]}\n",
|
| 263 |
-
" return a list of {'sensors': {...}, 'anomaly_prob': float}\n",
|
| 264 |
-
" log_prob(observations) \u2014 return a float\n",
|
| 265 |
-
"\n",
|
| 266 |
-
"Optional but rewarded:\n",
|
| 267 |
-
" detect_drift(history) -> bool\n",
|
| 268 |
-
" describe() -> str\n",
|
| 269 |
-
"\n",
|
| 270 |
-
"Allowed imports: numpy, scipy, math, statistics, collections, typing, random, functools, itertools\n",
|
| 271 |
-
"DO NOT import anything else. DO NOT use file I/O, network, or system calls.\n",
|
| 272 |
-
"Shorter correct theories score higher than longer ones (MDL principle).\n",
|
| 273 |
-
"Reply with ONLY the Python code \u2014 no markdown fences, no explanation.\"\"\"\n",
|
| 274 |
-
"\n",
|
| 275 |
-
"\n",
|
| 276 |
-
"def make_chat_prompt(obs_text: str, tokenizer) -> str:\n",
|
| 277 |
-
" messages = [\n",
|
| 278 |
-
" {'role': 'system', 'content': SYSTEM_PROMPT},\n",
|
| 279 |
-
" {'role': 'user', 'content': obs_text},\n",
|
| 280 |
-
" ]\n",
|
| 281 |
-
" return tokenizer.apply_chat_template(\n",
|
| 282 |
-
" messages, tokenize=False, add_generation_prompt=True\n",
|
| 283 |
-
" )\n",
|
| 284 |
-
"\n",
|
| 285 |
-
"\n",
|
| 286 |
-
"# We load raw rows first; prompts are formatted after the tokenizer is loaded\n",
|
| 287 |
-
"raw_rows = [json.loads(l) for l in CURRICULUM_PATH.read_text().splitlines() if l.strip()]\n",
|
| 288 |
-
"print(f'Loaded {len(raw_rows)} rows from curriculum')"
|
| 289 |
-
]
|
| 290 |
-
},
|
| 291 |
-
{
|
| 292 |
-
"cell_type": "markdown",
|
| 293 |
-
"id": "md-s5",
|
| 294 |
-
"metadata": {},
|
| 295 |
-
"source": [
|
| 296 |
-
"## Step 5 \u2014 Load Model with Unsloth"
|
| 297 |
-
]
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
"cell_type": "code",
|
| 301 |
-
"execution_count": null,
|
| 302 |
-
"id": "load-model",
|
| 303 |
-
"metadata": {},
|
| 304 |
-
"outputs": [],
|
| 305 |
-
"source": [
|
| 306 |
-
"model, tokenizer = FastLanguageModel.from_pretrained(\n",
|
| 307 |
-
" model_name = MODEL_NAME,\n",
|
| 308 |
-
" max_seq_length = MAX_SEQ_LEN,\n",
|
| 309 |
-
" dtype = None, # auto: bf16 on Ampere+, fp16 otherwise\n",
|
| 310 |
-
" load_in_4bit = True,\n",
|
| 311 |
-
")\n",
|
| 312 |
-
"\n",
|
| 313 |
-
"model = FastLanguageModel.get_peft_model(\n",
|
| 314 |
-
" model,\n",
|
| 315 |
-
" r = 16,\n",
|
| 316 |
-
" target_modules = ['q_proj', 'k_proj', 'v_proj', 'o_proj',\n",
|
| 317 |
-
" 'gate_proj', 'up_proj', 'down_proj'],\n",
|
| 318 |
-
" lora_alpha = 16,\n",
|
| 319 |
-
" lora_dropout = 0.05,\n",
|
| 320 |
-
" bias = 'none',\n",
|
| 321 |
-
" use_gradient_checkpointing = 'unsloth',\n",
|
| 322 |
-
" random_state = SEED,\n",
|
| 323 |
-
")\n",
|
| 324 |
-
"\n",
|
| 325 |
-
"total = sum(p.numel() for p in model.parameters())\n",
|
| 326 |
-
"trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 327 |
-
"print(f'Model loaded: {total:,} total params, {trainable:,} trainable ({100*trainable/total:.2f}%)')\n",
|
| 328 |
-
"\n",
|
| 329 |
-
"# Ensure pad token is set (needed for left-padding in GRPO)\n",
|
| 330 |
-
"if tokenizer.pad_token is None:\n",
|
| 331 |
-
" tokenizer.pad_token = tokenizer.eos_token\n",
|
| 332 |
-
"tokenizer.padding_side = 'left'"
|
| 333 |
-
]
|
| 334 |
-
},
|
| 335 |
-
{
|
| 336 |
-
"cell_type": "markdown",
|
| 337 |
-
"id": "md-s6",
|
| 338 |
-
"metadata": {},
|
| 339 |
-
"source": [
|
| 340 |
-
"## Step 6 \u2014 Format Dataset with Chat Template"
|
| 341 |
-
]
|
| 342 |
-
},
|
| 343 |
-
{
|
| 344 |
-
"cell_type": "code",
|
| 345 |
-
"execution_count": null,
|
| 346 |
-
"id": "format-dataset",
|
| 347 |
-
"metadata": {},
|
| 348 |
-
"outputs": [],
|
| 349 |
-
"source": [
|
| 350 |
-
"dataset_rows = []\n",
|
| 351 |
-
"for row in raw_rows:\n",
|
| 352 |
-
" dataset_rows.append({\n",
|
| 353 |
-
" 'prompt' : make_chat_prompt(row['prompt'], tokenizer),\n",
|
| 354 |
-
" 'future_json' : row['future_json'],\n",
|
| 355 |
-
" 'history_json' : row.get('history_json', '[]'),\n",
|
| 356 |
-
" 'sensor_names': json.dumps(row['sensor_names']),\n",
|
| 357 |
-
" })\n",
|
| 358 |
-
"\n",
|
| 359 |
-
"dataset = Dataset.from_list(dataset_rows)\n",
|
| 360 |
-
"print(f'Dataset: {len(dataset)} rows')\n",
|
| 361 |
-
"print(f'Columns: {dataset.column_names}')\n",
|
| 362 |
-
"print(f'Sample prompt length: {len(dataset[0][\"prompt\"])} chars')"
|
| 363 |
-
]
|
| 364 |
-
},
|
| 365 |
-
{
|
| 366 |
-
"cell_type": "markdown",
|
| 367 |
-
"id": "md-s7",
|
| 368 |
-
"metadata": {},
|
| 369 |
-
"source": [
|
| 370 |
-
"## Step 7 \u2014 GRPO Reward Function\n\n",
|
| 371 |
-
"Each completion is scored by running it through the project's real sandbox and scorer against\n",
|
| 372 |
-
"the **pre-captured future window** stored in the dataset row. This avoids the stale-trajectory\n",
|
| 373 |
-
"bug in the original notebook where the reward used a separately-seeded universe."
|
| 374 |
-
]
|
| 375 |
-
},
|
| 376 |
-
{
|
| 377 |
-
"cell_type": "code",
|
| 378 |
-
"execution_count": null,
|
| 379 |
-
"id": "reward-fn",
|
| 380 |
-
"metadata": {},
|
| 381 |
-
"outputs": [],
|
| 382 |
-
"source": [
|
| 383 |
-
"from ontology_architect.universe import SensorRecord\n",
|
| 384 |
-
"\n",
|
| 385 |
-
"_sandbox = TheorySandbox(ENV_CONFIG.sandbox)\n",
|
| 386 |
-
"_reward_cfg = ENV_CONFIG.reward\n",
|
| 387 |
-
"\n",
|
| 388 |
-
"\n",
|
| 389 |
-
"def _deserialise_future(future_json: str) -> list[SensorRecord]:\n",
|
| 390 |
-
" return [\n",
|
| 391 |
-
" SensorRecord(t=r['t'], sensors=r['sensors'], anomaly=r['anomaly'], drift=r['drift'])\n",
|
| 392 |
-
" for r in json.loads(future_json)\n",
|
| 393 |
-
" ]\n",
|
| 394 |
-
"\n",
|
| 395 |
-
"\n",
|
| 396 |
-
"def physics_reward_fn(\n",
|
| 397 |
-
" prompts : list[str],\n",
|
| 398 |
-
" completions : list[str],\n",
|
| 399 |
-
" future_json : list[str],\n",
|
| 400 |
-
" history_json: list[str],\n",
|
| 401 |
-
" sensor_names: list[str],\n",
|
| 402 |
-
" **kwargs,\n",
|
| 403 |
-
") -> list[float]:\n",
|
| 404 |
-
" \"\"\"\n",
|
| 405 |
-
" GRPO reward function. Columns `future_json` and `sensor_names` are\n",
|
| 406 |
-
" passed automatically by GRPOTrainer from the dataset.\n",
|
| 407 |
-
" \"\"\"\n",
|
| 408 |
-
" rewards = []\n",
|
| 409 |
-
" for theory_code, fj, hj_str, sn_json in zip(completions, future_json, history_json, sensor_names):\n",
|
| 410 |
-
" try:\n",
|
| 411 |
-
" # Strip markdown fences the model sometimes emits\n",
|
| 412 |
-
" theory_code = re.sub(r'^```(?:python)?\\s*', '', theory_code.strip())\n",
|
| 413 |
-
" theory_code = re.sub(r'```\\s*$', '', theory_code)\n",
|
| 414 |
-
"\n",
|
| 415 |
-
" future = _deserialise_future(fj)\n",
|
| 416 |
-
" names = tuple(json.loads(sn_json))\n",
|
| 417 |
-
" history_data = json.loads(hj_str) if hj_str else []\n",
|
| 418 |
-
" sandbox_res = _sandbox.execute(theory_code, history_data, future, names)\n",
|
| 419 |
-
" breakdown = score_theory(sandbox_res, future, theory_code, _reward_cfg)\n",
|
| 420 |
-
" reward = float(np.clip(breakdown.reward, -30.0, 5.0))\n",
|
| 421 |
-
" except Exception:\n",
|
| 422 |
-
" reward = float(_reward_cfg.execution_error_penalty)\n",
|
| 423 |
-
" rewards.append(reward)\n",
|
| 424 |
-
" return rewards\n",
|
| 425 |
-
"\n",
|
| 426 |
-
"\n",
|
| 427 |
-
"# Sanity check \u2014 teacher should score positively\n",
|
| 428 |
-
"row0 = dataset_rows[0]\n",
|
| 429 |
-
"test_rewards = physics_reward_fn(\n",
|
| 430 |
-
" prompts = [row0['prompt']],\n",
|
| 431 |
-
" completions = [get_baseline('teacher')],\n",
|
| 432 |
-
" future_json = [row0['future_json']],\n",
|
| 433 |
-
" sensor_names = [row0['sensor_names']],\n",
|
| 434 |
-
")\n",
|
| 435 |
-
"print(f'Sanity check \u2014 teacher reward on row 0: {test_rewards[0]:.4f}')\n",
|
| 436 |
-
"print(f' (Expected roughly > {teacher_mean:.2f} for a good theory)')"
|
| 437 |
-
]
|
| 438 |
-
},
|
| 439 |
-
{
|
| 440 |
-
"cell_type": "markdown",
|
| 441 |
-
"id": "md-s8",
|
| 442 |
-
"metadata": {},
|
| 443 |
-
"source": [
|
| 444 |
-
"## Step 8 \u2014 GRPO Training"
|
| 445 |
-
]
|
| 446 |
-
},
|
| 447 |
-
{
|
| 448 |
-
"cell_type": "code",
|
| 449 |
-
"execution_count": null,
|
| 450 |
-
"id": "train",
|
| 451 |
-
"metadata": {},
|
| 452 |
-
"outputs": [],
|
| 453 |
-
"source": [
|
| 454 |
-
"if USE_WANDB:\n",
|
| 455 |
-
" import wandb\n",
|
| 456 |
-
" wandb.init(project='alien-physics-discovery', name='qwen2.5-coder-1.5b-grpo')\n",
|
| 457 |
-
"\n",
|
| 458 |
-
"grpo_config = GRPOConfig(\n",
|
| 459 |
-
" output_dir = 'artifacts/checkpoints/grpo',\n",
|
| 460 |
-
" num_train_epochs = TRAIN_EPOCHS,\n",
|
| 461 |
-
" per_device_train_batch_size = BATCH_SIZE,\n",
|
| 462 |
-
" gradient_accumulation_steps = GRAD_ACCUM,\n",
|
| 463 |
-
" learning_rate = LR,\n",
|
| 464 |
-
" warmup_ratio = 0.05,\n",
|
| 465 |
-
" lr_scheduler_type = 'cosine',\n",
|
| 466 |
-
" num_generations = NUM_GENS,\n",
|
| 467 |
-
" max_completion_length = MAX_COMPLETION, # correct TRL >=0.9 field\n",
|
| 468 |
-
" logging_steps = 5,\n",
|
| 469 |
-
" save_steps = 50,\n",
|
| 470 |
-
" save_total_limit = 2,\n",
|
| 471 |
-
" fp16 = not torch.cuda.is_bf16_supported(),\n",
|
| 472 |
-
" bf16 = torch.cuda.is_bf16_supported(),\n",
|
| 473 |
-
" report_to = 'wandb' if USE_WANDB else 'none',\n",
|
| 474 |
-
" beta = 0.001,\n",
|
| 475 |
-
" max_grad_norm = 0.3,\n",
|
| 476 |
-
")\n",
|
| 477 |
-
"\n",
|
| 478 |
-
"trainer = GRPOTrainer(\n",
|
| 479 |
-
" model = model,\n",
|
| 480 |
-
" processing_class = tokenizer,\n",
|
| 481 |
-
" args = grpo_config, # 'args', not 'config'\n",
|
| 482 |
-
" train_dataset = dataset,\n",
|
| 483 |
-
" reward_funcs = [physics_reward_fn],\n",
|
| 484 |
-
")\n",
|
| 485 |
-
"\n",
|
| 486 |
-
"print('Starting GRPO training')\n",
|
| 487 |
-
"print(f' Model : {MODEL_NAME}')\n",
|
| 488 |
-
"print(f' Samples : {len(dataset)}')\n",
|
| 489 |
-
"print(f' Epochs : {TRAIN_EPOCHS}')\n",
|
| 490 |
-
"print(f' Group size : {NUM_GENS}')\n",
|
| 491 |
-
"print(f' Batch\u00d7accum : {BATCH_SIZE}\u00d7{GRAD_ACCUM} = {BATCH_SIZE*GRAD_ACCUM} effective')\n",
|
| 492 |
-
"\n",
|
| 493 |
-
"train_result = trainer.train()\n",
|
| 494 |
-
"print(f'\\nTraining complete \u2014 final loss: {train_result.training_loss:.4f}')"
|
| 495 |
-
]
|
| 496 |
-
},
|
| 497 |
-
{
|
| 498 |
-
"cell_type": "markdown",
|
| 499 |
-
"id": "md-s9",
|
| 500 |
-
"metadata": {},
|
| 501 |
-
"source": [
|
| 502 |
-
"## Step 9 \u2014 Plot Training Curves"
|
| 503 |
-
]
|
| 504 |
-
},
|
| 505 |
-
{
|
| 506 |
-
"cell_type": "code",
|
| 507 |
-
"execution_count": null,
|
| 508 |
-
"id": "plot",
|
| 509 |
-
"metadata": {},
|
| 510 |
-
"outputs": [],
|
| 511 |
-
"source": [
|
| 512 |
-
"logs = trainer.state.log_history\n",
|
| 513 |
-
"steps = [l['step'] for l in logs if 'loss' in l]\n",
|
| 514 |
-
"losses = [l['loss'] for l in logs if 'loss' in l]\n",
|
| 515 |
-
"r_pairs = [(l['step'], l['reward']) for l in logs if 'reward' in l]\n",
|
| 516 |
-
"\n",
|
| 517 |
-
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n",
|
| 518 |
-
"fig.suptitle('Alien Physics Discovery \u2014 GRPO Training (Qwen2.5-Coder-1.5B)', fontweight='bold')\n",
|
| 519 |
-
"\n",
|
| 520 |
-
"ax1.plot(steps, losses, color='#4f86c6', linewidth=2)\n",
|
| 521 |
-
"ax1.set_xlabel('Training Step')\n",
|
| 522 |
-
"ax1.set_ylabel('Loss')\n",
|
| 523 |
-
"ax1.set_title('GRPO Loss')\n",
|
| 524 |
-
"ax1.grid(alpha=0.3)\n",
|
| 525 |
-
"\n",
|
| 526 |
-
"if r_pairs:\n",
|
| 527 |
-
" rs, rv = zip(*r_pairs)\n",
|
| 528 |
-
" ax2.plot(rs, rv, color='#e67e22', linewidth=2, label='Mean group reward')\n",
|
| 529 |
-
" ax2.axhline(teacher_mean, color='gray', linestyle='--', alpha=0.7,\n",
|
| 530 |
-
" label=f'Teacher baseline ({teacher_mean:.2f})')\n",
|
| 531 |
-
" ax2.set_xlabel('Training Step')\n",
|
| 532 |
-
" ax2.set_ylabel('MDL Reward')\n",
|
| 533 |
-
" ax2.set_title('Reward vs Teacher Baseline')\n",
|
| 534 |
-
" ax2.legend()\n",
|
| 535 |
-
" ax2.grid(alpha=0.3)\n",
|
| 536 |
-
"else:\n",
|
| 537 |
-
" ax2.text(0.5, 0.5, 'Enable USE_WANDB for live\\nreward curves, or check\\nlog_history manually',\n",
|
| 538 |
-
" ha='center', va='center', transform=ax2.transAxes, color='gray')\n",
|
| 539 |
-
"\n",
|
| 540 |
-
"plt.tight_layout()\n",
|
| 541 |
-
"plt.savefig('artifacts/training_curves.png', dpi=150, bbox_inches='tight')\n",
|
| 542 |
-
"plt.show()\n",
|
| 543 |
-
"print('Saved artifacts/training_curves.png')"
|
| 544 |
-
]
|
| 545 |
-
},
|
| 546 |
-
{
|
| 547 |
-
"cell_type": "markdown",
|
| 548 |
-
"id": "md-s10",
|
| 549 |
-
"metadata": {},
|
| 550 |
-
"source": [
|
| 551 |
-
"## Step 10 \u2014 Before / After Evaluation"
|
| 552 |
-
]
|
| 553 |
-
},
|
| 554 |
-
{
|
| 555 |
-
"cell_type": "code",
|
| 556 |
-
"execution_count": null,
|
| 557 |
-
"id": "eval",
|
| 558 |
-
"metadata": {},
|
| 559 |
-
"outputs": [],
|
| 560 |
-
"source": [
|
| 561 |
-
"FastLanguageModel.for_inference(model)\n",
|
| 562 |
-
"\n",
|
| 563 |
-
"def generate_theory(obs_text: str, max_new: int = 480) -> str:\n",
|
| 564 |
-
" prompt = make_chat_prompt(obs_text, tokenizer)\n",
|
| 565 |
-
" inputs = tokenizer(prompt, return_tensors='pt',\n",
|
| 566 |
-
" truncation=True, max_length=MAX_SEQ_LEN - max_new).to(model.device)\n",
|
| 567 |
-
" with torch.no_grad():\n",
|
| 568 |
-
" out = model.generate(\n",
|
| 569 |
-
" **inputs,\n",
|
| 570 |
-
" max_new_tokens = max_new,\n",
|
| 571 |
-
" temperature = 0.6,\n",
|
| 572 |
-
" do_sample = True,\n",
|
| 573 |
-
" pad_token_id = tokenizer.eos_token_id,\n",
|
| 574 |
-
" )\n",
|
| 575 |
-
" theory = tokenizer.decode(out[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)\n",
|
| 576 |
-
" theory = re.sub(r'^```(?:python)?\\s*', '', theory.strip())\n",
|
| 577 |
-
" theory = re.sub(r'```\\s*$', '', theory)\n",
|
| 578 |
-
" return theory\n",
|
| 579 |
-
"\n",
|
| 580 |
-
"\n",
|
| 581 |
-
"def eval_full_episode(family: str, seed: int) -> dict:\n",
|
| 582 |
-
" from dataclasses import replace as dc_replace\n",
|
| 583 |
-
" cfg = ExperimentConfig(\n",
|
| 584 |
-
" universe=dc_replace(ENV_CONFIG.universe, family=family, seed=seed),\n",
|
| 585 |
-
" sandbox=ENV_CONFIG.sandbox,\n",
|
| 586 |
-
" reward=ENV_CONFIG.reward,\n",
|
| 587 |
-
" feedback=ENV_CONFIG.feedback,\n",
|
| 588 |
-
" )\n",
|
| 589 |
-
" env = OntologyArchitectEnvironment(cfg)\n",
|
| 590 |
-
" obs = env.reset()\n",
|
| 591 |
-
" rewards, done = [], False\n",
|
| 592 |
-
" final_theory = ''\n",
|
| 593 |
-
" while not done:\n",
|
| 594 |
-
" theory = generate_theory(obs.text)\n",
|
| 595 |
-
" obs = env.step(OntologyArchitectAction(\n",
|
| 596 |
-
" theory_module=theory, revision_note='eval'\n",
|
| 597 |
-
" ))\n",
|
| 598 |
-
" rewards.append(obs.reward)\n",
|
| 599 |
-
" final_theory = theory\n",
|
| 600 |
-
" done = obs.done\n",
|
| 601 |
-
" return {'family': family, 'rewards': rewards,\n",
|
| 602 |
-
" 'mean_reward': float(np.mean(rewards)), 'final_theory': final_theory}\n",
|
| 603 |
-
"\n",
|
| 604 |
-
"\n",
|
| 605 |
-
"# Teacher baseline (untrained)\n",
|
| 606 |
-
"def eval_teacher_episode(family: str, seed: int) -> dict:\n",
|
| 607 |
-
" from dataclasses import replace as dc_replace\n",
|
| 608 |
-
" cfg = ExperimentConfig(\n",
|
| 609 |
-
" universe=dc_replace(ENV_CONFIG.universe, family=family, seed=seed),\n",
|
| 610 |
-
" sandbox=ENV_CONFIG.sandbox,\n",
|
| 611 |
-
" reward=ENV_CONFIG.reward,\n",
|
| 612 |
-
" feedback=ENV_CONFIG.feedback,\n",
|
| 613 |
-
" )\n",
|
| 614 |
-
" env = OntologyArchitectEnvironment(cfg)\n",
|
| 615 |
-
" obs = env.reset()\n",
|
| 616 |
-
" teacher = get_baseline('teacher')\n",
|
| 617 |
-
" rewards, done = [], False\n",
|
| 618 |
-
" while not done:\n",
|
| 619 |
-
" obs = env.step(OntologyArchitectAction(theory_module=teacher, revision_note='baseline'))\n",
|
| 620 |
-
" rewards.append(obs.reward)\n",
|
| 621 |
-
" done = obs.done\n",
|
| 622 |
-
" return {'family': family, 'rewards': rewards, 'mean_reward': float(np.mean(rewards))}\n",
|
| 623 |
-
"\n",
|
| 624 |
-
"\n",
|
| 625 |
-
"print('Running eval episodes (trained model vs teacher baseline)...')\n",
|
| 626 |
-
"eval_seeds = [9001, 9002]\n",
|
| 627 |
-
"eval_families = ['thermal_split', 'coupled_oscillator']\n",
|
| 628 |
-
"\n",
|
| 629 |
-
"trained_results = [eval_full_episode(f, s) for f in eval_families for s in eval_seeds]\n",
|
| 630 |
-
"baseline_results = [eval_teacher_episode(f, s) for f in eval_families for s in eval_seeds]\n",
|
| 631 |
-
"\n",
|
| 632 |
-
"trained_mean = float(np.mean([r['mean_reward'] for r in trained_results]))\n",
|
| 633 |
-
"baseline_mean = float(np.mean([r['mean_reward'] for r in baseline_results]))\n",
|
| 634 |
-
"\n",
|
| 635 |
-
"print(f'\\nResults:')\n",
|
| 636 |
-
"print(f' Trained model mean reward : {trained_mean:.4f}')\n",
|
| 637 |
-
"print(f' Teacher baseline mean reward: {baseline_mean:.4f}')\n",
|
| 638 |
-
"print(f' Delta : {trained_mean - baseline_mean:+.4f}')"
|
| 639 |
-
]
|
| 640 |
-
},
|
| 641 |
-
{
|
| 642 |
-
"cell_type": "markdown",
|
| 643 |
-
"id": "md-s11",
|
| 644 |
-
"metadata": {},
|
| 645 |
-
"source": [
|
| 646 |
-
"## Step 11 \u2014 Per-Turn Improvement Plot"
|
| 647 |
-
]
|
| 648 |
-
},
|
| 649 |
-
{
|
| 650 |
-
"cell_type": "code",
|
| 651 |
-
"execution_count": null,
|
| 652 |
-
"id": "per-turn-plot",
|
| 653 |
-
"metadata": {},
|
| 654 |
-
"outputs": [],
|
| 655 |
-
"source": [
|
| 656 |
-
"# Aggregate rewards by turn index across all eval episodes\n",
|
| 657 |
-
"max_turns = max(len(r['rewards']) for r in trained_results)\n",
|
| 658 |
-
"\n",
|
| 659 |
-
"def pad_rewards(results, length):\n",
|
| 660 |
-
" padded = [r['rewards'] + [r['rewards'][-1]] * (length - len(r['rewards'])) for r in results]\n",
|
| 661 |
-
" return np.array(padded)\n",
|
| 662 |
-
"\n",
|
| 663 |
-
"trained_mat = pad_rewards(trained_results, max_turns)\n",
|
| 664 |
-
"baseline_mat = pad_rewards(baseline_results, max_turns)\n",
|
| 665 |
-
"turns = list(range(1, max_turns + 1))\n",
|
| 666 |
-
"\n",
|
| 667 |
-
"fig, ax = plt.subplots(figsize=(8, 4))\n",
|
| 668 |
-
"ax.plot(turns, trained_mat.mean(0), color='#3498db', marker='o', lw=2, label='Trained model')\n",
|
| 669 |
-
"ax.fill_between(turns,\n",
|
| 670 |
-
" trained_mat.mean(0) - trained_mat.std(0),\n",
|
| 671 |
-
" trained_mat.mean(0) + trained_mat.std(0),\n",
|
| 672 |
-
" alpha=0.15, color='#3498db')\n",
|
| 673 |
-
"ax.plot(turns, baseline_mat.mean(0), color='gray', marker='s', lw=2, linestyle='--', label='Teacher baseline')\n",
|
| 674 |
-
"ax.set_xlabel('Refinement Turn')\n",
|
| 675 |
-
"ax.set_ylabel('MDL Reward')\n",
|
| 676 |
-
"ax.set_title('Per-Turn Theory Improvement')\n",
|
| 677 |
-
"ax.set_xticks(turns)\n",
|
| 678 |
-
"ax.legend()\n",
|
| 679 |
-
"ax.grid(alpha=0.3)\n",
|
| 680 |
-
"plt.tight_layout()\n",
|
| 681 |
-
"plt.savefig('artifacts/per_turn_improvement.png', dpi=150, bbox_inches='tight')\n",
|
| 682 |
-
"plt.show()\n",
|
| 683 |
-
"print('Saved artifacts/per_turn_improvement.png')"
|
| 684 |
-
]
|
| 685 |
-
},
|
| 686 |
-
{
|
| 687 |
-
"cell_type": "markdown",
|
| 688 |
-
"id": "md-s12",
|
| 689 |
-
"metadata": {},
|
| 690 |
-
"source": [
|
| 691 |
-
"## Step 12 \u2014 Show a Discovered Theory"
|
| 692 |
-
]
|
| 693 |
-
},
|
| 694 |
-
{
|
| 695 |
-
"cell_type": "code",
|
| 696 |
-
"execution_count": null,
|
| 697 |
-
"id": "show-theory",
|
| 698 |
-
"metadata": {},
|
| 699 |
-
"outputs": [],
|
| 700 |
-
"source": [
|
| 701 |
-
"best = max(trained_results, key=lambda r: r['mean_reward'])\n",
|
| 702 |
-
"print(f\"Best result \u2014 family={best['family']} mean_reward={best['mean_reward']:.4f}\")\n",
|
| 703 |
-
"print('=' * 70)\n",
|
| 704 |
-
"theory = best['final_theory']\n",
|
| 705 |
-
"print(theory[:2000])\n",
|
| 706 |
-
"if len(theory) > 2000:\n",
|
| 707 |
-
" print(f'\\n... [{len(theory)-2000} more chars] ...')"
|
| 708 |
-
]
|
| 709 |
-
},
|
| 710 |
-
{
|
| 711 |
-
"cell_type": "markdown",
|
| 712 |
-
"id": "md-s13",
|
| 713 |
-
"metadata": {},
|
| 714 |
-
"source": [
|
| 715 |
-
"## Step 13 \u2014 Save Model & Push to Hub"
|
| 716 |
-
]
|
| 717 |
-
},
|
| 718 |
-
{
|
| 719 |
-
"cell_type": "code",
|
| 720 |
-
"execution_count": null,
|
| 721 |
-
"id": "save",
|
| 722 |
-
"metadata": {},
|
| 723 |
-
"outputs": [],
|
| 724 |
-
"source": [
|
| 725 |
-
"Path('artifacts').mkdir(exist_ok=True)\n",
|
| 726 |
-
"model.save_pretrained('artifacts/model')\n",
|
| 727 |
-
"tokenizer.save_pretrained('artifacts/model')\n",
|
| 728 |
-
"print('Model saved to artifacts/model')\n",
|
| 729 |
-
"\n",
|
| 730 |
-
"if HF_TOKEN:\n",
|
| 731 |
-
" from huggingface_hub import login\n",
|
| 732 |
-
" login(token=HF_TOKEN)\n",
|
| 733 |
-
" model.push_to_hub('lostdecimal27/alien-physics-discovery-qwen2.5-1.5b')\n",
|
| 734 |
-
" tokenizer.push_to_hub('lostdecimal27/alien-physics-discovery-qwen2.5-1.5b')\n",
|
| 735 |
-
" print('Pushed to HuggingFace Hub')\n",
|
| 736 |
-
"else:\n",
|
| 737 |
-
" print('Set HF_TOKEN above to push to Hub')"
|
| 738 |
-
]
|
| 739 |
-
},
|
| 740 |
-
{
|
| 741 |
-
"cell_type": "markdown",
|
| 742 |
-
"id": "md-s14",
|
| 743 |
-
"metadata": {},
|
| 744 |
-
"source": [
|
| 745 |
-
"## Step 14 \u2014 Submission Checklist"
|
| 746 |
-
]
|
| 747 |
-
},
|
| 748 |
-
{
|
| 749 |
-
"cell_type": "code",
|
| 750 |
-
"execution_count": null,
|
| 751 |
-
"id": "checklist",
|
| 752 |
-
"metadata": {},
|
| 753 |
-
"outputs": [],
|
| 754 |
-
"source": [
|
| 755 |
-
"checks = {\n",
|
| 756 |
-
" 'artifacts/training_curves.png' : Path('artifacts/training_curves.png').exists(),\n",
|
| 757 |
-
" 'artifacts/per_turn_improvement.png': Path('artifacts/per_turn_improvement.png').exists(),\n",
|
| 758 |
-
" 'artifacts/model (local save)' : Path('artifacts/model').exists(),\n",
|
| 759 |
-
" 'openenv.yaml in repo' : Path('openenv.yaml').exists(),\n",
|
| 760 |
-
" 'README.md in repo' : Path('README.md').exists(),\n",
|
| 761 |
-
" 'Colab notebook (this file)' : True,\n",
|
| 762 |
-
"}\n",
|
| 763 |
-
"\n",
|
| 764 |
-
"print('OpenEnv Hackathon \u2014 Submission Checklist')\n",
|
| 765 |
-
"print('=' * 50)\n",
|
| 766 |
-
"for item, ok in checks.items():\n",
|
| 767 |
-
" print(f\" {'\u2705' if ok else '\u274c'} {item}\")\n",
|
| 768 |
-
"\n",
|
| 769 |
-
"print('\\nRemaining manual steps:')\n",
|
| 770 |
-
"for todo in [\n",
|
| 771 |
-
" '\u25a1 Push model to HuggingFace Hub (set HF_TOKEN)',\n",
|
| 772 |
-
" '\u25a1 Deploy environment to HuggingFace Space',\n",
|
| 773 |
-
" '\u25a1 Record < 2 min YouTube demo or write HF blog post',\n",
|
| 774 |
-
" '\u25a1 Update lostdecimal27 placeholders above',\n",
|
| 775 |
-
" '\u25a1 Update README with video/blog URL and HF Space URL',\n",
|
| 776 |
-
" '\u25a1 Submit HF Space URL to hackathon portal',\n",
|
| 777 |
-
"]:\n",
|
| 778 |
-
" print(f' {todo}')\n",
|
| 779 |
-
"\n",
|
| 780 |
-
"print(f'\\nFinal score summary:')\n",
|
| 781 |
-
"print(f' Teacher baseline mean reward : {baseline_mean:.4f}')\n",
|
| 782 |
-
"print(f' Trained model mean reward : {trained_mean:.4f}')\n",
|
| 783 |
-
"print(f' Improvement : {trained_mean - baseline_mean:+.4f}')"
|
| 784 |
-
]
|
| 785 |
-
}
|
| 786 |
-
]
|
| 787 |
-
}
|
|
|
|
| 1 |
+
{"nbformat": 4, "nbformat_minor": 5, "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}, "colab": {"provenance": [], "gpuType": "T4"}, "accelerator": "GPU"}, "cells": [{"cell_type": "markdown", "id": "md-title", "metadata": {}, "source": ["# Alien Physics Discovery \u2014 OpenEnv Hackathon 2026\n", "### Train an LLM to do unsupervised scientific discovery from raw sensor data\n", "\n", "**Theme:** Wild Card (Theme 5) + World Modeling (Theme 3.1) + Self-Improvement (Theme 4)\n", "\n", "**The Problem:** LLMs can write code and explain physics, but have never been trained to\n", "*discover* physical structure from raw, ambiguous time-series data through iterative theory revision.\n", "\n", "**The Environment:** An agent observes noisy sensor logs from a simulated alien universe\n", "governed by hidden ODEs. It must write a Python `Theory` class that models the dynamics.\n", "The environment executes the code against future observations. Reward = log-likelihood \u2212 MDL penalty.\n", "\n", "---\n", "> \u26a1 **Runtime:** T4 GPU (free Colab tier works) \n", "> \ud83d\udce6 **Stack:** Unsloth + HF TRL (GRPO) + project's sandboxed execution"]}, {"cell_type": "markdown", "id": "md-s1", "metadata": {}, "source": ["## Step 1 \u2014 Install Dependencies"]}, {"cell_type": "code", "execution_count": null, "id": "install", "metadata": {}, "outputs": [], "source": ["%%capture\n", "# Unsloth must come first \u2014 it patches transformers in-place\n", "!pip install \"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\"\n", "!pip install \"trl>=0.9.0\" peft accelerate bitsandbytes datasets wandb huggingface_hub\n", "!pip install matplotlib numpy scipy\n", "\n", "# Install the Ontology Architect environment from the repo\n", "# Replace with your actual repo URL before submitting\n", "!pip install \"git+https://github.com/rohan-27p/ontology_architect.git\"\n", "\n", "print('All dependencies installed')"]}, {"cell_type": "markdown", "id": "md-s2", "metadata": {}, "source": ["## Step 2 \u2014 Imports & Config"]}, {"cell_type": "code", "execution_count": null, "id": "imports", "metadata": {}, "outputs": [], "source": ["import json, os, re, warnings\n", "from pathlib import Path\n", "\n", "import numpy as np\n", "import torch\n", "from datasets import Dataset\n", "from unsloth import FastLanguageModel\n", "from trl import GRPOConfig, GRPOTrainer\n", "import matplotlib.pyplot as plt\n", "\n", "warnings.filterwarnings('ignore')\n", "\n", "# \u2500\u2500 Project imports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "from ontology_architect.baselines import get_baseline\n", "from ontology_architect.config import (\n", " ExperimentConfig, FeedbackConfig, RewardConfig, SandboxConfig, UniverseConfig\n", ")\n", "from ontology_architect.models import OntologyArchitectAction\n", "from ontology_architect.sandbox import TheorySandbox\n", "from ontology_architect.reward import score_theory\n", "from ontology_architect.server.ontology_architect_environment import OntologyArchitectEnvironment\n", "from ontology_architect.universe import ProceduralAlienUniverse\n", "\n", "# \u2500\u2500 Reproducibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "SEED = 42\n", "np.random.seed(SEED)\n", "torch.manual_seed(SEED)\n", "\n", "# \u2500\u2500 Model \u2014 Qwen2.5-Coder-1.5B fits T4 (16 GB) comfortably \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "MODEL_NAME = 'unsloth/Qwen2.5-Coder-7B-Instruct-bnb-4bit'\n", "MAX_SEQ_LEN = 2048\n", "\n", "# \u2500\u2500 Training knobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "NUM_EPISODES = 60 # oracle curriculum episodes\n", "TRAIN_EPOCHS = 2\n", "NUM_GENS = 4 # GRPO group size \u2014 2 keeps T4 safe\n", "MAX_COMPLETION = 768 # theory code tokens\n", "BATCH_SIZE = 1\n", "GRAD_ACCUM = 8\n", "LR = 2e-5\n", "USE_WANDB = False\n", "HF_TOKEN = '' # set to push model to Hub\n", "\n", "# \u2500\u2500 Environment config \u2014 matches tiny_smoke but longer for richer signal \u2500\u2500\n", "ENV_CONFIG = ExperimentConfig(\n", " universe=UniverseConfig(\n", " seed=SEED,\n", " family='dual_fluid',\n", " observation_window=12,\n", " future_window=4,\n", " max_steps=4,\n", " anomaly_rate=0.06,\n", " drift_interval=6,\n", " ),\n", " sandbox=SandboxConfig(mode='subprocess', timeout_seconds=4.0),\n", " reward=RewardConfig(\n", " mdl_lambda=0.0005,\n", " prediction_sigma=0.12,\n", " anomaly_bonus=2.0,\n", " drift_bonus=1.0,\n", " execution_error_penalty=-25.0,\n", " ),\n", " feedback=FeedbackConfig(peer_review_window=4, lineage_window=4),\n", ")\n", "\n", "print(f'Config ready')\n", "print(f' Model : {MODEL_NAME}')\n", "print(f' Device : {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"}')\n", "print(f' VRAM : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB' if torch.cuda.is_available() else '')"]}, {"cell_type": "markdown", "id": "md-s3", "metadata": {}, "source": ["## Step 3 \u2014 Generate Oracle Curriculum\n\nThe teacher baseline rolls out `NUM_EPISODES` episodes. Each step saves `(prompt, completion, reward, future_records)`. The `future_records` are the ground-truth sensor windows \u2014 we store them so the GRPO reward function can score any new theory against the exact same future without re-running the environment."]}, {"cell_type": "code", "execution_count": null, "id": "curriculum", "metadata": {}, "outputs": [], "source": ["CURRICULUM_PATH = Path('artifacts/curriculum/grpo_curriculum.jsonl')\n", "CURRICULUM_PATH.parent.mkdir(parents=True, exist_ok=True)\n", "\n", "FAMILIES = ['dual_fluid', 'coupled_oscillator']\n", "teacher = get_baseline('teacher')\n", "sandbox = TheorySandbox(ENV_CONFIG.sandbox)\n", "\n", "examples = []\n", "\n", "with CURRICULUM_PATH.open('w') as fh:\n", " for ep in range(NUM_EPISODES):\n", " # Rotate through families so the model sees both universes\n", " family = FAMILIES[ep % len(FAMILIES)]\n", " cfg = ExperimentConfig(\n", " universe=UniverseConfig(\n", " seed=SEED + ep * 997,\n", " family=family,\n", " observation_window=ENV_CONFIG.universe.observation_window,\n", " future_window=ENV_CONFIG.universe.future_window,\n", " max_steps=ENV_CONFIG.universe.max_steps,\n", " anomaly_rate=ENV_CONFIG.universe.anomaly_rate,\n", " drift_interval=ENV_CONFIG.universe.drift_interval,\n", " ),\n", " sandbox=ENV_CONFIG.sandbox,\n", " reward=ENV_CONFIG.reward,\n", " feedback=ENV_CONFIG.feedback,\n", " )\n", " env = OntologyArchitectEnvironment(cfg)\n", " obs = env.reset()\n", "\n", " for step in range(cfg.universe.max_steps):\n", " # Capture the future window that this step will be scored against\n", " cursor = env._cursor\n", " future_records = env._records[cursor : cursor + cfg.universe.future_window]\n", " sensor_names = list(env._universe.sensor_names)\n", "\n", " row = {\n", " 'prompt' : obs.text,\n", " 'completion' : teacher,\n", " 'episode' : ep,\n", " 'step' : step,\n", " 'family' : family,\n", " 'sensor_names' : sensor_names,\n", " # Serialise future so reward_fn can use it without re-running env\n", " 'future_json' : json.dumps([\n", " {'t': r.t, 'sensors': r.sensors, 'anomaly': r.anomaly, 'drift': r.drift}\n", " for r in future_records\n", " ]),\n", " }\n", "\n", " obs = env.step(OntologyArchitectAction(\n", " theory_module=teacher,\n", " revision_note=f'curriculum ep={ep} step={step}',\n", " ))\n", " row['reward'] = obs.reward\n", " row['metrics'] = json.dumps(obs.metadata.get('last_metrics', {}))\n", "\n", " fh.write(json.dumps(row) + '\\n')\n", " examples.append(row)\n", "\n", " if obs.done:\n", " break\n", "\n", "print(f'Generated {len(examples)} curriculum examples across {NUM_EPISODES} episodes')\n", "print(f' Families: {set(e[\"family\"] for e in examples)}')\n", "print(f' Reward range: [{min(e[\"reward\"] for e in examples):.3f}, {max(e[\"reward\"] for e in examples):.3f}]')\n", "teacher_mean = float(np.mean([e['reward'] for e in examples]))\n", "print(f' Teacher mean reward: {teacher_mean:.4f} (this is the bar GRPO must beat)')"]}, {"cell_type": "markdown", "id": "md-s4", "metadata": {}, "source": ["## Step 4 \u2014 Build HuggingFace Dataset"]}, {"cell_type": "code", "execution_count": null, "id": "dataset", "metadata": {}, "outputs": [], "source": ["SYSTEM_PROMPT = \"\"\"You are a scientific discovery agent. An alien universe is governed by hidden physical laws.\n", "You observe noisy sensor logs and must write a compact Python Theory class that predicts future readings.\n", "\n", "Your class MUST implement:\n", " fit(history) \u2014 history is a list of dicts: [{'sensors': {'sigma': float, ...}}, ...]\n", " predict(window) \u2014 window is {'steps': int, 'history': [...], 'sensor_names': [...]}\n", " return a list of {'sensors': {...}, 'anomaly_prob': float}\n", " log_prob(observations) \u2014 return a float\n", "\n", "Optional but rewarded:\n", " detect_drift(history) -> bool\n", " describe() -> str\n", "\n", "Allowed imports: numpy, scipy, math, statistics, collections, typing, random, functools, itertools\n", "DO NOT import anything else. DO NOT use file I/O, network, or system calls.\n", "Shorter correct theories score higher than longer ones (MDL principle).\n", "Reply with ONLY the Python code \u2014 no markdown fences, no explanation.\"\"\"\n", "\n", "\n", "def make_chat_prompt(obs_text: str, tokenizer) -> str:\n", " messages = [\n", " {'role': 'system', 'content': SYSTEM_PROMPT},\n", " {'role': 'user', 'content': obs_text},\n", " ]\n", " return tokenizer.apply_chat_template(\n", " messages, tokenize=False, add_generation_prompt=True\n", " )\n", "\n", "\n", "# We load raw rows first; prompts are formatted after the tokenizer is loaded\n", "raw_rows = [json.loads(l) for l in CURRICULUM_PATH.read_text().splitlines() if l.strip()]\n", "print(f'Loaded {len(raw_rows)} rows from curriculum')"]}, {"cell_type": "markdown", "id": "md-s5", "metadata": {}, "source": ["## Step 5 \u2014 Load Model with Unsloth"]}, {"cell_type": "code", "execution_count": null, "id": "load-model", "metadata": {}, "outputs": [], "source": ["model, tokenizer = FastLanguageModel.from_pretrained(\n", " model_name = MODEL_NAME,\n", " max_seq_length = MAX_SEQ_LEN,\n", " dtype = None, # auto: bf16 on Ampere+, fp16 otherwise\n", " load_in_4bit = True,\n", ")\n", "\n", "model = FastLanguageModel.get_peft_model(\n", " model,\n", " r = 16,\n", " target_modules = ['q_proj', 'k_proj', 'v_proj', 'o_proj',\n", " 'gate_proj', 'up_proj', 'down_proj'],\n", " lora_alpha = 16,\n", " lora_dropout = 0.05,\n", " bias = 'none',\n", " use_gradient_checkpointing = 'unsloth',\n", " random_state = SEED,\n", ")\n", "\n", "total = sum(p.numel() for p in model.parameters())\n", "trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\n", "print(f'Model loaded: {total:,} total params, {trainable:,} trainable ({100*trainable/total:.2f}%)')\n", "\n", "# Ensure pad token is set (needed for left-padding in GRPO)\n", "if tokenizer.pad_token is None:\n", " tokenizer.pad_token = tokenizer.eos_token\n", "tokenizer.padding_side = 'left'"]}, {"cell_type": "markdown", "id": "md-s6", "metadata": {}, "source": ["## Step 6 \u2014 Format Dataset with Chat Template"]}, {"cell_type": "code", "execution_count": null, "id": "format-dataset", "metadata": {}, "outputs": [], "source": ["dataset_rows = []\n", "for row in raw_rows:\n", " dataset_rows.append({\n", " 'prompt' : make_chat_prompt(row['prompt'], tokenizer),\n", " 'future_json' : row['future_json'],\n", " 'history_json' : row.get('history_json', '[]'),\n", " 'sensor_names': json.dumps(row['sensor_names']),\n", " })\n", "\n", "dataset = Dataset.from_list(dataset_rows)\n", "print(f'Dataset: {len(dataset)} rows')\n", "print(f'Columns: {dataset.column_names}')\n", "print(f'Sample prompt length: {len(dataset[0][\"prompt\"])} chars')"]}, {"cell_type": "markdown", "id": "md-s7", "metadata": {}, "source": ["## Step 7 \u2014 GRPO Reward Function\n\n", "Each completion is scored by running it through the project's real sandbox and scorer against\n", "the **pre-captured future window** stored in the dataset row. This avoids the stale-trajectory\n", "bug in the original notebook where the reward used a separately-seeded universe."]}, {"cell_type": "code", "execution_count": null, "id": "reward-fn", "metadata": {}, "outputs": [], "source": ["from ontology_architect.universe import SensorRecord\n", "\n", "_sandbox = TheorySandbox(ENV_CONFIG.sandbox)\n", "_reward_cfg = ENV_CONFIG.reward\n", "\n", "\n", "def _deserialise_future(future_json: str) -> list[SensorRecord]:\n", " return [\n", " SensorRecord(t=r['t'], sensors=r['sensors'], anomaly=r['anomaly'], drift=r['drift'])\n", " for r in json.loads(future_json)\n", " ]\n", "\n", "\n", "def physics_reward_fn(\n", " prompts : list[str],\n", " completions : list[str],\n", " future_json : list[str],\n", " history_json: list[str],\n", " sensor_names: list[str],\n", " **kwargs,\n", ") -> list[float]:\n", " \"\"\"\n", " GRPO reward function. Columns `future_json` and `sensor_names` are\n", " passed automatically by GRPOTrainer from the dataset.\n", " \"\"\"\n", " rewards = []\n", " for theory_code, fj, hj_str, sn_json in zip(completions, future_json, history_json, sensor_names):\n", " try:\n", " # Strip markdown fences the model sometimes emits\n", " theory_code = re.sub(r'^```(?:python)?\\s*', '', theory_code.strip())\n", " theory_code = re.sub(r'```\\s*$', '', theory_code)\n", "\n", " future = _deserialise_future(fj)\n", " names = tuple(json.loads(sn_json))\n", " history_data = json.loads(hj_str) if hj_str else []\n", " sandbox_res = _sandbox.execute(theory_code, history_data, future, names)\n", " breakdown = score_theory(sandbox_res, future, theory_code, _reward_cfg)\n", " reward = float(np.clip(breakdown.reward, -30.0, 5.0))\n", " except Exception:\n", " reward = float(_reward_cfg.execution_error_penalty)\n", " rewards.append(reward)\n", " return rewards\n", "\n", "\n", "# Sanity check \u2014 teacher should score positively\n", "row0 = dataset_rows[0]\n", "test_rewards = physics_reward_fn(\n", " prompts = [row0['prompt']],\n", " completions = [get_baseline('teacher')],\n", " future_json = [row0['future_json']],\n", " sensor_names = [row0['sensor_names']],\n", ")\n", "print(f'Sanity check \u2014 teacher reward on row 0: {test_rewards[0]:.4f}')\n", "print(f' (Expected roughly > {teacher_mean:.2f} for a good theory)')"]}, {"cell_type": "markdown", "id": "md-s8", "metadata": {}, "source": ["## Step 8 \u2014 GRPO Training"]}, {"cell_type": "code", "execution_count": null, "id": "train", "metadata": {}, "outputs": [], "source": ["if USE_WANDB:\n", " import wandb\n", " wandb.init(project='alien-physics-discovery', name='qwen2.5-coder-1.5b-grpo')\n", "\n", "grpo_config = GRPOConfig(\n", " output_dir = 'artifacts/checkpoints/grpo',\n", " num_train_epochs = TRAIN_EPOCHS,\n", " per_device_train_batch_size = BATCH_SIZE,\n", " gradient_accumulation_steps = GRAD_ACCUM,\n", " learning_rate = LR,\n", " warmup_ratio = 0.05,\n", " lr_scheduler_type = 'cosine',\n", " num_generations = NUM_GENS,\n", " max_completion_length = MAX_COMPLETION, # correct TRL >=0.9 field\n", " logging_steps = 5,\n", " save_steps = 50,\n", " save_total_limit = 2,\n", " fp16 = not torch.cuda.is_bf16_supported(),\n", " bf16 = torch.cuda.is_bf16_supported(),\n", " report_to = 'wandb' if USE_WANDB else 'none',\n", " beta = 0.001,\n", " max_grad_norm = 0.3,\n", ")\n", "\n", "trainer = GRPOTrainer(\n", " model = model,\n", " processing_class = tokenizer,\n", " args = grpo_config, # 'args', not 'config'\n", " train_dataset = dataset,\n", " reward_funcs = [physics_reward_fn],\n", ")\n", "\n", "print('Starting GRPO training')\n", "print(f' Model : {MODEL_NAME}')\n", "print(f' Samples : {len(dataset)}')\n", "print(f' Epochs : {TRAIN_EPOCHS}')\n", "print(f' Group size : {NUM_GENS}')\n", "print(f' Batch\u00d7accum : {BATCH_SIZE}\u00d7{GRAD_ACCUM} = {BATCH_SIZE*GRAD_ACCUM} effective')\n", "\n", "train_result = trainer.train()\n", "print(f'\\nTraining complete \u2014 final loss: {train_result.training_loss:.4f}')"]}, {"cell_type": "markdown", "id": "md-s9", "metadata": {}, "source": ["## Step 9 \u2014 Plot Training Curves"]}, {"cell_type": "code", "execution_count": null, "id": "plot", "metadata": {}, "outputs": [], "source": ["logs = trainer.state.log_history\n", "steps = [l['step'] for l in logs if 'loss' in l]\n", "losses = [l['loss'] for l in logs if 'loss' in l]\n", "r_pairs = [(l['step'], l['reward']) for l in logs if 'reward' in l]\n", "\n", "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", "fig.suptitle('Alien Physics Discovery \u2014 GRPO Training (Qwen2.5-Coder-1.5B)', fontweight='bold')\n", "\n", "ax1.plot(steps, losses, color='#4f86c6', linewidth=2)\n", "ax1.set_xlabel('Training Step')\n", "ax1.set_ylabel('Loss')\n", "ax1.set_title('GRPO Loss')\n", "ax1.grid(alpha=0.3)\n", "\n", "if r_pairs:\n", " rs, rv = zip(*r_pairs)\n", " ax2.plot(rs, rv, color='#e67e22', linewidth=2, label='Mean group reward')\n", " ax2.axhline(teacher_mean, color='gray', linestyle='--', alpha=0.7,\n", " label=f'Teacher baseline ({teacher_mean:.2f})')\n", " ax2.set_xlabel('Training Step')\n", " ax2.set_ylabel('MDL Reward')\n", " ax2.set_title('Reward vs Teacher Baseline')\n", " ax2.legend()\n", " ax2.grid(alpha=0.3)\n", "else:\n", " ax2.text(0.5, 0.5, 'Enable USE_WANDB for live\\nreward curves, or check\\nlog_history manually',\n", " ha='center', va='center', transform=ax2.transAxes, color='gray')\n", "\n", "plt.tight_layout()\n", "plt.savefig('artifacts/training_curves.png', dpi=150, bbox_inches='tight')\n", "plt.show()\n", "print('Saved artifacts/training_curves.png')"]}, {"cell_type": "markdown", "id": "md-s10", "metadata": {}, "source": ["## Step 10 \u2014 Before / After Evaluation"]}, {"cell_type": "code", "execution_count": null, "id": "eval", "metadata": {}, "outputs": [], "source": ["FastLanguageModel.for_inference(model)\n", "\n", "def generate_theory(obs_text: str, max_new: int = 480) -> str:\n", " prompt = make_chat_prompt(obs_text, tokenizer)\n", " inputs = tokenizer(prompt, return_tensors='pt',\n", " truncation=True, max_length=MAX_SEQ_LEN - max_new).to(model.device)\n", " with torch.no_grad():\n", " out = model.generate(\n", " **inputs,\n", " max_new_tokens = max_new,\n", " temperature = 0.6,\n", " do_sample = True,\n", " pad_token_id = tokenizer.eos_token_id,\n", " )\n", " theory = tokenizer.decode(out[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)\n", " theory = re.sub(r'^```(?:python)?\\s*', '', theory.strip())\n", " theory = re.sub(r'```\\s*$', '', theory)\n", " return theory\n", "\n", "\n", "def eval_full_episode(family: str, seed: int) -> dict:\n", " from dataclasses import replace as dc_replace\n", " cfg = ExperimentConfig(\n", " universe=dc_replace(ENV_CONFIG.universe, family=family, seed=seed),\n", " sandbox=ENV_CONFIG.sandbox,\n", " reward=ENV_CONFIG.reward,\n", " feedback=ENV_CONFIG.feedback,\n", " )\n", " env = OntologyArchitectEnvironment(cfg)\n", " obs = env.reset()\n", " rewards, done = [], False\n", " final_theory = ''\n", " while not done:\n", " theory = generate_theory(obs.text)\n", " obs = env.step(OntologyArchitectAction(\n", " theory_module=theory, revision_note='eval'\n", " ))\n", " rewards.append(obs.reward)\n", " final_theory = theory\n", " done = obs.done\n", " return {'family': family, 'rewards': rewards,\n", " 'mean_reward': float(np.mean(rewards)), 'final_theory': final_theory}\n", "\n", "\n", "# Teacher baseline (untrained)\n", "def eval_teacher_episode(family: str, seed: int) -> dict:\n", " from dataclasses import replace as dc_replace\n", " cfg = ExperimentConfig(\n", " universe=dc_replace(ENV_CONFIG.universe, family=family, seed=seed),\n", " sandbox=ENV_CONFIG.sandbox,\n", " reward=ENV_CONFIG.reward,\n", " feedback=ENV_CONFIG.feedback,\n", " )\n", " env = OntologyArchitectEnvironment(cfg)\n", " obs = env.reset()\n", " teacher = get_baseline('teacher')\n", " rewards, done = [], False\n", " while not done:\n", " obs = env.step(OntologyArchitectAction(theory_module=teacher, revision_note='baseline'))\n", " rewards.append(obs.reward)\n", " done = obs.done\n", " return {'family': family, 'rewards': rewards, 'mean_reward': float(np.mean(rewards))}\n", "\n", "\n", "print('Running eval episodes (trained model vs teacher baseline)...')\n", "eval_seeds = [9001, 9002]\n", "eval_families = ['thermal_split', 'coupled_oscillator']\n", "\n", "trained_results = [eval_full_episode(f, s) for f in eval_families for s in eval_seeds]\n", "baseline_results = [eval_teacher_episode(f, s) for f in eval_families for s in eval_seeds]\n", "\n", "trained_mean = float(np.mean([r['mean_reward'] for r in trained_results]))\n", "baseline_mean = float(np.mean([r['mean_reward'] for r in baseline_results]))\n", "\n", "print(f'\\nResults:')\n", "print(f' Trained model mean reward : {trained_mean:.4f}')\n", "print(f' Teacher baseline mean reward: {baseline_mean:.4f}')\n", "print(f' Delta : {trained_mean - baseline_mean:+.4f}')"]}, {"cell_type": "markdown", "id": "md-s11", "metadata": {}, "source": ["## Step 11 \u2014 Per-Turn Improvement Plot"]}, {"cell_type": "code", "execution_count": null, "id": "per-turn-plot", "metadata": {}, "outputs": [], "source": ["# Aggregate rewards by turn index across all eval episodes\n", "max_turns = max(len(r['rewards']) for r in trained_results)\n", "\n", "def pad_rewards(results, length):\n", " padded = [r['rewards'] + [r['rewards'][-1]] * (length - len(r['rewards'])) for r in results]\n", " return np.array(padded)\n", "\n", "trained_mat = pad_rewards(trained_results, max_turns)\n", "baseline_mat = pad_rewards(baseline_results, max_turns)\n", "turns = list(range(1, max_turns + 1))\n", "\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", "ax.plot(turns, trained_mat.mean(0), color='#3498db', marker='o', lw=2, label='Trained model')\n", "ax.fill_between(turns,\n", " trained_mat.mean(0) - trained_mat.std(0),\n", " trained_mat.mean(0) + trained_mat.std(0),\n", " alpha=0.15, color='#3498db')\n", "ax.plot(turns, baseline_mat.mean(0), color='gray', marker='s', lw=2, linestyle='--', label='Teacher baseline')\n", "ax.set_xlabel('Refinement Turn')\n", "ax.set_ylabel('MDL Reward')\n", "ax.set_title('Per-Turn Theory Improvement')\n", "ax.set_xticks(turns)\n", "ax.legend()\n", "ax.grid(alpha=0.3)\n", "plt.tight_layout()\n", "plt.savefig('artifacts/per_turn_improvement.png', dpi=150, bbox_inches='tight')\n", "plt.show()\n", "print('Saved artifacts/per_turn_improvement.png')"]}, {"cell_type": "markdown", "id": "md-s12", "metadata": {}, "source": ["## Step 12 \u2014 Show a Discovered Theory"]}, {"cell_type": "code", "execution_count": null, "id": "show-theory", "metadata": {}, "outputs": [], "source": ["best = max(trained_results, key=lambda r: r['mean_reward'])\n", "print(f\"Best result \u2014 family={best['family']} mean_reward={best['mean_reward']:.4f}\")\n", "print('=' * 70)\n", "theory = best['final_theory']\n", "print(theory[:2000])\n", "if len(theory) > 2000:\n", " print(f'\\n... [{len(theory)-2000} more chars] ...')"]}, {"cell_type": "markdown", "id": "md-s13", "metadata": {}, "source": ["## Step 13 \u2014 Save Model & Push to Hub"]}, {"cell_type": "code", "execution_count": null, "id": "save", "metadata": {}, "outputs": [], "source": ["Path('artifacts').mkdir(exist_ok=True)\n", "model.save_pretrained('artifacts/model')\n", "tokenizer.save_pretrained('artifacts/model')\n", "print('Model saved to artifacts/model')\n", "\n", "if HF_TOKEN:\n", " from huggingface_hub import login\n", " login(token=HF_TOKEN)\n", " model.push_to_hub('lostdecimal27/alien-physics-discovery-qwen2.5-1.5b')\n", " tokenizer.push_to_hub('lostdecimal27/alien-physics-discovery-qwen2.5-1.5b')\n", " print('Pushed to HuggingFace Hub')\n", "else:\n", " print('Set HF_TOKEN above to push to Hub')"]}, {"cell_type": "markdown", "id": "md-s14", "metadata": {}, "source": ["## Step 14 \u2014 Submission Checklist"]}, {"cell_type": "code", "execution_count": null, "id": "checklist", "metadata": {}, "outputs": [], "source": ["checks = {\n", " 'artifacts/training_curves.png' : Path('artifacts/training_curves.png').exists(),\n", " 'artifacts/per_turn_improvement.png': Path('artifacts/per_turn_improvement.png').exists(),\n", " 'artifacts/model (local save)' : Path('artifacts/model').exists(),\n", " 'openenv.yaml in repo' : Path('openenv.yaml').exists(),\n", " 'README.md in repo' : Path('README.md').exists(),\n", " 'Colab notebook (this file)' : True,\n", "}\n", "\n", "print('OpenEnv Hackathon \u2014 Submission Checklist')\n", "print('=' * 50)\n", "for item, ok in checks.items():\n", " print(f\" {'\u2705' if ok else '\u274c'} {item}\")\n", "\n", "print('\\nRemaining manual steps:')\n", "for todo in [\n", " '\u25a1 Push model to HuggingFace Hub (set HF_TOKEN)',\n", " '\u25a1 Deploy environment to HuggingFace Space',\n", " '\u25a1 Record < 2 min YouTube demo or write HF blog post',\n", " '\u25a1 Update lostdecimal27 placeholders above',\n", " '\u25a1 Update README with video/blog URL and HF Space URL',\n", " '\u25a1 Submit HF Space URL to hackathon portal',\n", "]:\n", " print(f' {todo}')\n", "\n", "print(f'\\nFinal score summary:')\n", "print(f' Teacher baseline mean reward : {baseline_mean:.4f}')\n", "print(f' Trained model mean reward : {trained_mean:.4f}')\n", "print(f' Improvement : {trained_mean - baseline_mean:+.4f}')"]}]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
artifacts/{discovery_plot.png → results/discovery_plot.png}
RENAMED
|
File without changes
|
artifacts/{grader_report.json → results/grader_report.json}
RENAMED
|
File without changes
|
artifacts/{reward_curve.png → results/reward_curve.png}
RENAMED
|
File without changes
|
baselines.py
CHANGED
|
@@ -143,9 +143,9 @@ DUAL_FLUID_DSL_ORACLE = _json.dumps({
|
|
| 143 |
"C": 0.0,
|
| 144 |
},
|
| 145 |
"dynamics": {
|
| 146 |
-
"A": {"linear": {"terms": {"A": -0.3, "
|
| 147 |
-
"B": {"add": [{"sin": "
|
| 148 |
-
"C": {"
|
| 149 |
},
|
| 150 |
"observations": {
|
| 151 |
"pressure": {"var": "A"},
|
|
|
|
| 143 |
"C": 0.0,
|
| 144 |
},
|
| 145 |
"dynamics": {
|
| 146 |
+
"A": {"add": [{"linear": {"terms": {"A": -0.3}}}, {"mul": [0.5, {"var": "A"}, {"var": "B"}]}]},
|
| 147 |
+
"B": {"add": [{"mul": [0.4, {"sin": {"var": "t"}}]}, {"mul": [-0.2, {"pow": [{"var": "A"}, 2]}]}]},
|
| 148 |
+
"C": {"add": [{"mul": [0.15, {"sin": {"mul": [0.4, {"var": "A"}]}}]}, {"linear": {"terms": {"C": -0.12}}}]},
|
| 149 |
},
|
| 150 |
"observations": {
|
| 151 |
"pressure": {"var": "A"},
|
scripts/visualize_discovery.py
CHANGED
|
@@ -73,8 +73,8 @@ def main() -> None:
|
|
| 73 |
|
| 74 |
# Run theory through sandbox to get predictions
|
| 75 |
sandbox = TheorySandbox(config.sandbox)
|
| 76 |
-
# Use
|
| 77 |
-
split =
|
| 78 |
history = records[:split]
|
| 79 |
future = records[split:]
|
| 80 |
|
|
|
|
| 73 |
|
| 74 |
# Run theory through sandbox to get predictions
|
| 75 |
sandbox = TheorySandbox(config.sandbox)
|
| 76 |
+
# Use a small initial window as history so the oracle can initialize correctly
|
| 77 |
+
split = 8
|
| 78 |
history = records[:split]
|
| 79 |
future = records[split:]
|
| 80 |
|
theory_dsl.py
CHANGED
|
@@ -72,6 +72,7 @@ def normalize_theory_spec(spec: dict[str, Any]) -> dict[str, Any]:
|
|
| 72 |
raise TheoryDSLValidationError(f"DSL observations exceed {MAX_SENSOR_OUTPUTS} outputs")
|
| 73 |
|
| 74 |
state_set = set(state)
|
|
|
|
| 75 |
for name, expr in dynamics.items():
|
| 76 |
if str(name) not in state_set:
|
| 77 |
raise TheoryDSLValidationError(f"Dynamic variable '{name}' is not in state")
|
|
@@ -284,7 +285,9 @@ class Theory:
|
|
| 284 |
recent = history[-lookback:] if history else []
|
| 285 |
self.trend = self._fit_trend(recent)
|
| 286 |
self.residual_scale = self._fit_residual_scale(recent)
|
|
|
|
| 287 |
self.state = self._initial_state()
|
|
|
|
| 288 |
return {
|
| 289 |
"dsl_version": self.spec["dsl_version"],
|
| 290 |
"state": list(self.spec["state"]),
|
|
@@ -387,6 +390,7 @@ class Theory:
|
|
| 387 |
for _ in range(max(substeps, 1)):
|
| 388 |
deriv = self._derivative(state)
|
| 389 |
state = {name: self._clip(value + step_dt * deriv.get(name, 0.0)) for name, value in state.items()}
|
|
|
|
| 390 |
return state
|
| 391 |
|
| 392 |
def _derivative(self, state):
|
|
|
|
| 72 |
raise TheoryDSLValidationError(f"DSL observations exceed {MAX_SENSOR_OUTPUTS} outputs")
|
| 73 |
|
| 74 |
state_set = set(state)
|
| 75 |
+
state_set.add("t")
|
| 76 |
for name, expr in dynamics.items():
|
| 77 |
if str(name) not in state_set:
|
| 78 |
raise TheoryDSLValidationError(f"Dynamic variable '{name}' is not in state")
|
|
|
|
| 285 |
recent = history[-lookback:] if history else []
|
| 286 |
self.trend = self._fit_trend(recent)
|
| 287 |
self.residual_scale = self._fit_residual_scale(recent)
|
| 288 |
+
self.last_t = float(history[-1].get("t", 0.0)) if history else 0.0
|
| 289 |
self.state = self._initial_state()
|
| 290 |
+
self.state["t"] = self.last_t
|
| 291 |
return {
|
| 292 |
"dsl_version": self.spec["dsl_version"],
|
| 293 |
"state": list(self.spec["state"]),
|
|
|
|
| 390 |
for _ in range(max(substeps, 1)):
|
| 391 |
deriv = self._derivative(state)
|
| 392 |
state = {name: self._clip(value + step_dt * deriv.get(name, 0.0)) for name, value in state.items()}
|
| 393 |
+
state["t"] = state.get("t", 0.0) + step_dt
|
| 394 |
return state
|
| 395 |
|
| 396 |
def _derivative(self, state):
|