adityss commited on
Commit
bdadba1
·
1 Parent(s): a4671c4

feat: add GridMind GRPO training notebook using Unsloth and HF TRL

Browse files
Files changed (1) hide show
  1. scripts/gridmind_grpo_colab.ipynb +115 -107
scripts/gridmind_grpo_colab.ipynb CHANGED
@@ -4,11 +4,13 @@
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
- "# ⚡ GridMind-RL: Training an LLM Energy Controller with Unsloth + GRPO\n",
8
  "\n",
9
  "This notebook fine-tunes **Qwen2.5-1.5B-Instruct** to manage industrial building energy\n",
10
  "using Reinforcement Learning via the live **GridMind-RL OpenEnv** environment.\n",
11
  "\n",
 
 
12
  "| | |\n",
13
  "|---|---|\n",
14
  "| **Environment** | https://lo-kyu-gridmind.hf.space |\n",
@@ -19,7 +21,7 @@
19
  "### What does the agent learn?\n",
20
  "- **Task 1**: Minimize energy cost by charging thermal storage off-peak\n",
21
  "- **Task 2**: Maintain indoor temperature while minimizing cost\n",
22
- "- **Task 3**: Full demand-response — cost + temperature + grid stress + batch scheduling + carbon"
23
  ]
24
  },
25
  {
@@ -29,7 +31,7 @@
29
  "outputs": [],
30
  "source": [
31
  "%%capture\n",
32
- "!pip install unsloth openenv-core\n",
33
  "!pip install --no-deps bitsandbytes accelerate xformers peft trl triton\n",
34
  "!pip install --no-deps cut_cross_entropy unsloth_zoo\n",
35
  "!pip install \"datasets>=3.4.1,<4.0.0\" pandas matplotlib nest_asyncio"
@@ -39,7 +41,7 @@
39
  "cell_type": "markdown",
40
  "metadata": {},
41
  "source": [
42
- "## Step 1 — Verify the Live Environment"
43
  ]
44
  },
45
  {
@@ -48,31 +50,34 @@
48
  "metadata": {},
49
  "outputs": [],
50
  "source": [
51
- "from openenv.core import GenericEnvClient\n",
52
- "import asyncio, nest_asyncio\n",
53
- "nest_asyncio.apply()\n",
54
  "\n",
55
  "ENV_URL = \"https://lo-kyu-gridmind.hf.space\"\n",
56
  "\n",
57
- "async def verify_env():\n",
58
- " async with GenericEnvClient(base_url=ENV_URL) as env:\n",
59
- " r = await env.reset()\n",
60
- " print(\"✅ Environment live!\")\n",
61
- " print(\"Observation keys:\", list(r.observation.keys()))\n",
62
- " r2 = await env.step({\n",
 
 
63
  " \"hvac_power_level\": 0.5, \"thermal_charge_rate\": 0.0,\n",
64
  " \"batch_job_slot\": 0, \"load_shed_fraction\": 0.0, \"building_id\": 0\n",
65
- " })\n",
66
- " print(f\"Step reward: {r2.reward:.3f}, done: {r2.done}\")\n",
 
 
 
67
  "\n",
68
- "asyncio.run(verify_env())"
69
  ]
70
  },
71
  {
72
  "cell_type": "markdown",
73
  "metadata": {},
74
  "source": [
75
- "## Step 2 — Load Model with Unsloth 4-bit LoRA"
76
  ]
77
  },
78
  {
@@ -102,14 +107,14 @@
102
  " use_gradient_checkpointing=\"unsloth\",\n",
103
  " random_state=42,\n",
104
  ")\n",
105
- "print(\"✅ Model loaded with Unsloth 4-bit LoRA\")"
106
  ]
107
  },
108
  {
109
  "cell_type": "markdown",
110
  "metadata": {},
111
  "source": [
112
- "## Step 3 — Define Reward Functions\n",
113
  "\n",
114
  "We use a **composite reward** with three components:\n",
115
  "\n",
@@ -127,14 +132,14 @@
127
  "metadata": {},
128
  "outputs": [],
129
  "source": [
130
- "import json, re\n",
131
  "\n",
132
  "def reward_valid_json(completions, **kwargs):\n",
133
  " rewards = []\n",
134
  " for completion in completions:\n",
135
  " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
136
  " try:\n",
137
- " match = re.search(r'\\{.*?\\}', text, re.DOTALL)\n",
138
  " if match:\n",
139
  " json.loads(match.group())\n",
140
  " rewards.append(0.3)\n",
@@ -150,7 +155,7 @@
150
  " for completion in completions:\n",
151
  " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
152
  " try:\n",
153
- " match = re.search(r'\\{.*?\\}', text, re.DOTALL)\n",
154
  " if match:\n",
155
  " action = json.loads(match.group())\n",
156
  " rewards.append(0.3 if required.issubset(action.keys()) else 0.1)\n",
@@ -162,9 +167,11 @@
162
  "\n",
163
  "def reward_env_interaction(completions, **kwargs):\n",
164
  " \"\"\"Reward 0.0-0.4 based on actual environment reward from live GridMind-RL HF Space.\"\"\"\n",
165
- " async def run_step(text):\n",
 
 
166
  " try:\n",
167
- " match = re.search(r'\\{.*?\\}', text, re.DOTALL)\n",
168
  " action = json.loads(match.group()) if match else {}\n",
169
  " step_action = {\n",
170
  " \"hvac_power_level\": float(max(0, min(1, action.get(\"hvac_power_level\", 0.5)))),\n",
@@ -173,21 +180,23 @@
173
  " \"load_shed_fraction\": float(max(0, min(0.5, action.get(\"load_shed_fraction\", 0.0)))),\n",
174
  " \"building_id\": 0\n",
175
  " }\n",
176
- " async with GenericEnvClient(base_url=ENV_URL) as env:\n",
177
- " await env.reset()\n",
178
- " result = await env.step(step_action)\n",
179
- " # Normalize raw env reward (~[-2, 3]) → (0.0, 0.4)\n",
180
- " return min(0.4, max(0.0, (result.reward + 2.0) * 0.08))\n",
 
 
 
 
 
 
 
181
  " except Exception:\n",
182
- " return 0.0\n",
183
- "\n",
184
- " rewards = []\n",
185
- " for completion in completions:\n",
186
- " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
187
- " rewards.append(asyncio.run(run_step(text)))\n",
188
  " return rewards\n",
189
  "\n",
190
- "print(\"✅ Reward functions defined\")\n",
191
  "print(\" Total max reward per step: 1.0\")"
192
  ]
193
  },
@@ -195,7 +204,7 @@
195
  "cell_type": "markdown",
196
  "metadata": {},
197
  "source": [
198
- "## Step 4 — Build Training Dataset & Start GRPO Training"
199
  ]
200
  },
201
  {
@@ -204,80 +213,79 @@
204
  "metadata": {},
205
  "outputs": [],
206
  "source": [
207
- "from trl import GRPOTrainer, GRPOConfig\n",
208
- "from datasets import Dataset\n",
209
- "import pandas as pd, os\n",
210
- "from transformers import TrainerCallback\n",
211
- "\n",
212
- "SYSTEM_PROMPT = \"\"\"You are an expert industrial building energy controller.\n",
213
- "Each turn you receive the current building state and must respond with \n",
214
- "ONLY a valid JSON action object.\n",
215
- "\n",
216
- "Action format:\n",
217
- "{\"hvac_power_level\": <0.0-1.0>, \"thermal_charge_rate\": <-1.0 to 1.0>, \n",
218
- " \"batch_job_slot\": <0-4>, \"load_shed_fraction\": <0.0-0.5>, \"building_id\": 0}\n",
219
  "\n",
220
- "Strategy:\n",
221
- "- Charge storage when price < $0.08/kWh (positive thermal_charge_rate)\n",
222
- "- Discharge storage when price > $0.15/kWh (negative thermal_charge_rate) \n",
223
- "- Shed load 0.3-0.5 when grid_stress_signal > 0.7\n",
224
- "- Reduce HVAC during peak hours (8-12, 17-21)\n",
225
- "- Keep temperature between 19-23°C\"\"\"\n",
226
- "\n",
227
- "def make_prompt(i):\n",
228
- " return [{\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
229
- " {\"role\": \"user\",\n",
230
- " \"content\": f\"Episode {i+1}: Building simulation starting. Output your first action as JSON.\"}]\n",
231
- "\n",
232
- "dataset = Dataset.from_dict({\"prompt\": [make_prompt(i) for i in range(300)]})\n",
233
- "print(f\"✅ Dataset: {len(dataset)} training prompts\")\n",
234
- "\n",
235
- "# --- CSV Logger ---\n",
236
- "log_history = []\n",
237
- "class CSVLogger(TrainerCallback):\n",
238
- " def on_log(self, args, state, control, logs=None, **kwargs):\n",
239
- " if logs and \"loss\" in logs:\n",
240
- " entry = {**logs, \"step\": state.global_step}\n",
241
- " log_history.append(entry)\n",
242
- " os.makedirs(\"results\", exist_ok=True)\n",
243
- " pd.DataFrame(log_history).to_csv(\"results/training_log.csv\", index=False)\n",
244
  "\n",
245
- "training_args = GRPOConfig(\n",
246
- " output_dir=\"gridmind-grpo-unsloth\",\n",
247
- " num_train_epochs=1,\n",
248
- " per_device_train_batch_size=1,\n",
249
- " gradient_accumulation_steps=4,\n",
250
- " num_generations=4,\n",
251
- " max_prompt_length=256,\n",
252
- " max_completion_length=128,\n",
253
- " learning_rate=5e-6,\n",
254
- " lr_scheduler_type=\"cosine\",\n",
255
- " warmup_ratio=0.1,\n",
256
- " logging_steps=5,\n",
257
- " save_steps=100,\n",
258
- " fp16=True,\n",
259
- " report_to=\"none\",\n",
260
- " seed=42,\n",
261
- ")\n",
262
  "\n",
263
- "trainer = GRPOTrainer(\n",
264
- " model=model,\n",
265
- " tokenizer=tokenizer,\n",
266
- " args=training_args,\n",
267
- " train_dataset=dataset,\n",
268
- " reward_funcs=[reward_valid_json, reward_has_required_keys, reward_env_interaction],\n",
269
- " callbacks=[CSVLogger()]\n",
270
- ")\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  "\n",
272
- "print(\"🚀 Starting GRPO training...\")\n",
273
- "trainer.train()"
274
  ]
275
  },
276
  {
277
  "cell_type": "markdown",
278
  "metadata": {},
279
  "source": [
280
- "## Step 5 — Plot Training Curve\n",
281
  "\n",
282
  "This plot is the key **evidence of learning** for the hackathon judges."
283
  ]
@@ -312,14 +320,14 @@
312
  "plt.tight_layout()\n",
313
  "plt.savefig(\"results/training_curve.png\", dpi=200, bbox_inches='tight')\n",
314
  "plt.show()\n",
315
- "print(\"✅ Training curve saved to results/training_curve.png\")"
316
  ]
317
  },
318
  {
319
  "cell_type": "markdown",
320
  "metadata": {},
321
  "source": [
322
- "## Step 6 — Before vs After Comparison\n",
323
  "\n",
324
  "Test the same scenario pre-training and post-training to show qualitative improvement."
325
  ]
@@ -331,7 +339,7 @@
331
  "outputs": [],
332
  "source": [
333
  "test_state = (\n",
334
- " \"Building state: temp=24.5°C (too hot!), price=$0.18/kWh (peak), \"\n",
335
  " \"storage=0.7 (charged), grid_stress=0.85 (CRITICAL!), hour=18, step=60/95\\n\"\n",
336
  " \"Pending batch job deadlines: [12, 30]\\n\"\n",
337
  " \"Cumulative cost so far: $1.24\"\n",
@@ -354,11 +362,11 @@
354
  " )\n",
355
  "\n",
356
  "response = tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True)\n",
357
- "print(\"📋 Test Scenario:\")\n",
358
  "print(\" \", test_state.replace(\"\\n\", \"\\n \"))\n",
359
- "print(\"\\n🤖 Fine-tuned Model Response:\")\n",
360
  "print(\" \", response)\n",
361
- "print(\"\\n✅ Expected: load_shed_fraction > 0 (grid_stress=0.85), thermal_charge_rate < 0 (discharge at peak price)\")"
362
  ]
363
  }
364
  ],
 
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
+ "# \u26a1 GridMind-RL: Training an LLM Energy Controller with Unsloth + GRPO\n",
8
  "\n",
9
  "This notebook fine-tunes **Qwen2.5-1.5B-Instruct** to manage industrial building energy\n",
10
  "using Reinforcement Learning via the live **GridMind-RL OpenEnv** environment.\n",
11
  "\n",
12
+ "**Hardware:** This notebook is designed to run on a **Hugging Face Space (ZeroGPU/A10G)** or Google Colab (T4/L4).\n",
13
+ "\n",
14
  "| | |\n",
15
  "|---|---|\n",
16
  "| **Environment** | https://lo-kyu-gridmind.hf.space |\n",
 
21
  "### What does the agent learn?\n",
22
  "- **Task 1**: Minimize energy cost by charging thermal storage off-peak\n",
23
  "- **Task 2**: Maintain indoor temperature while minimizing cost\n",
24
+ "- **Task 3**: Full demand-response \u2014 cost + temperature + grid stress + batch scheduling + carbon"
25
  ]
26
  },
27
  {
 
31
  "outputs": [],
32
  "source": [
33
  "%%capture\n",
34
+ "!pip install unsloth requests\n",
35
  "!pip install --no-deps bitsandbytes accelerate xformers peft trl triton\n",
36
  "!pip install --no-deps cut_cross_entropy unsloth_zoo\n",
37
  "!pip install \"datasets>=3.4.1,<4.0.0\" pandas matplotlib nest_asyncio"
 
41
  "cell_type": "markdown",
42
  "metadata": {},
43
  "source": [
44
+ "## Step 1 \u2014 Verify the Live Environment"
45
  ]
46
  },
47
  {
 
50
  "metadata": {},
51
  "outputs": [],
52
  "source": [
53
+ "import requests\n",
 
 
54
  "\n",
55
  "ENV_URL = \"https://lo-kyu-gridmind.hf.space\"\n",
56
  "\n",
57
+ "def verify_env():\n",
58
+ " try:\n",
59
+ " r = requests.post(f\"{ENV_URL}/reset\", json={\"task_id\": 1, \"seed\": 42})\n",
60
+ " r.raise_for_status()\n",
61
+ " data = r.json()\n",
62
+ " print(\"\u2705 Environment live!\")\n",
63
+ " print(\"Observation keys:\", list(data.get(\"observations\", [{}])[0].keys()))\n",
64
+ " r2 = requests.post(f\"{ENV_URL}/step\", json=[{\n",
65
  " \"hvac_power_level\": 0.5, \"thermal_charge_rate\": 0.0,\n",
66
  " \"batch_job_slot\": 0, \"load_shed_fraction\": 0.0, \"building_id\": 0\n",
67
+ " }])\n",
68
+ " res = r2.json().get(\"results\", [{}])[0]\n",
69
+ " print(f\"Step reward: {res.get('reward', 0):.3f}, done: {res.get('done', False)}\")\n",
70
+ " except Exception as e:\n",
71
+ " print(f\"\u274c Environment verification failed: {e}\")\n",
72
  "\n",
73
+ "verify_env()"
74
  ]
75
  },
76
  {
77
  "cell_type": "markdown",
78
  "metadata": {},
79
  "source": [
80
+ "## Step 2 \u2014 Load Model with Unsloth 4-bit LoRA"
81
  ]
82
  },
83
  {
 
107
  " use_gradient_checkpointing=\"unsloth\",\n",
108
  " random_state=42,\n",
109
  ")\n",
110
+ "print(\"\u2705 Model loaded with Unsloth 4-bit LoRA\")"
111
  ]
112
  },
113
  {
114
  "cell_type": "markdown",
115
  "metadata": {},
116
  "source": [
117
+ "## Step 3 \u2014 Define Reward Functions\n",
118
  "\n",
119
  "We use a **composite reward** with three components:\n",
120
  "\n",
 
132
  "metadata": {},
133
  "outputs": [],
134
  "source": [
135
+ "import json, re, requests\n",
136
  "\n",
137
  "def reward_valid_json(completions, **kwargs):\n",
138
  " rewards = []\n",
139
  " for completion in completions:\n",
140
  " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
141
  " try:\n",
142
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
143
  " if match:\n",
144
  " json.loads(match.group())\n",
145
  " rewards.append(0.3)\n",
 
155
  " for completion in completions:\n",
156
  " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
157
  " try:\n",
158
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
159
  " if match:\n",
160
  " action = json.loads(match.group())\n",
161
  " rewards.append(0.3 if required.issubset(action.keys()) else 0.1)\n",
 
167
  "\n",
168
  "def reward_env_interaction(completions, **kwargs):\n",
169
  " \"\"\"Reward 0.0-0.4 based on actual environment reward from live GridMind-RL HF Space.\"\"\"\n",
170
+ " rewards = []\n",
171
+ " for completion in completions:\n",
172
+ " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
173
  " try:\n",
174
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
175
  " action = json.loads(match.group()) if match else {}\n",
176
  " step_action = {\n",
177
  " \"hvac_power_level\": float(max(0, min(1, action.get(\"hvac_power_level\", 0.5)))),\n",
 
180
  " \"load_shed_fraction\": float(max(0, min(0.5, action.get(\"load_shed_fraction\", 0.0)))),\n",
181
  " \"building_id\": 0\n",
182
  " }\n",
183
+ " r_reset = requests.post(f\"{ENV_URL}/reset\", json={\"task_id\": 1, \"seed\": 42}, timeout=30)\n",
184
+ " if r_reset.status_code != 200:\n",
185
+ " rewards.append(0.0)\n",
186
+ " continue\n",
187
+ " r_step = requests.post(f\"{ENV_URL}/step\", json=[step_action], timeout=30)\n",
188
+ " if r_step.status_code != 200:\n",
189
+ " rewards.append(0.0)\n",
190
+ " continue\n",
191
+ " res = r_step.json().get(\"results\", [{}])[0]\n",
192
+ " step_reward = float(res.get(\"reward\", 0.0))\n",
193
+ " val = (step_reward + 2.0) * 0.08\n",
194
+ " rewards.append(min(0.4, max(0.0, val)))\n",
195
  " except Exception:\n",
196
+ " rewards.append(0.0)\n",
 
 
 
 
 
197
  " return rewards\n",
198
  "\n",
199
+ "print(\"\u2705 Reward functions defined\")\n",
200
  "print(\" Total max reward per step: 1.0\")"
201
  ]
202
  },
 
204
  "cell_type": "markdown",
205
  "metadata": {},
206
  "source": [
207
+ "## Step 4 \u2014 Build Training Dataset & Start GRPO Training"
208
  ]
209
  },
210
  {
 
213
  "metadata": {},
214
  "outputs": [],
215
  "source": [
216
+ "import json, re, requests\n",
 
 
 
 
 
 
 
 
 
 
 
217
  "\n",
218
+ "def reward_valid_json(completions, **kwargs):\n",
219
+ " rewards = []\n",
220
+ " for completion in completions:\n",
221
+ " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
222
+ " try:\n",
223
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
224
+ " if match:\n",
225
+ " json.loads(match.group())\n",
226
+ " rewards.append(0.3)\n",
227
+ " else:\n",
228
+ " rewards.append(0.0)\n",
229
+ " except Exception:\n",
230
+ " rewards.append(0.0)\n",
231
+ " return rewards\n",
 
 
 
 
 
 
 
 
 
 
232
  "\n",
233
+ "def reward_has_required_keys(completions, **kwargs):\n",
234
+ " required = {\"hvac_power_level\", \"thermal_charge_rate\", \"batch_job_slot\", \"load_shed_fraction\"}\n",
235
+ " rewards = []\n",
236
+ " for completion in completions:\n",
237
+ " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
238
+ " try:\n",
239
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
240
+ " if match:\n",
241
+ " action = json.loads(match.group())\n",
242
+ " rewards.append(0.3 if required.issubset(action.keys()) else 0.1)\n",
243
+ " else:\n",
244
+ " rewards.append(0.0)\n",
245
+ " except Exception:\n",
246
+ " rewards.append(0.0)\n",
247
+ " return rewards\n",
 
 
248
  "\n",
249
+ "def reward_env_interaction(completions, **kwargs):\n",
250
+ " \"\"\"Reward 0.0-0.4 based on actual environment reward from live GridMind-RL HF Space.\"\"\"\n",
251
+ " rewards = []\n",
252
+ " for completion in completions:\n",
253
+ " text = completion[0][\"content\"] if isinstance(completion, list) else completion\n",
254
+ " try:\n",
255
+ " match = re.search(r\"\\{.*?\\}\", text, re.DOTALL)\n",
256
+ " action = json.loads(match.group()) if match else {}\n",
257
+ " step_action = {\n",
258
+ " \"hvac_power_level\": float(max(0, min(1, action.get(\"hvac_power_level\", 0.5)))),\n",
259
+ " \"thermal_charge_rate\": float(max(-1, min(1, action.get(\"thermal_charge_rate\", 0.0)))),\n",
260
+ " \"batch_job_slot\": int(max(0, min(4, action.get(\"batch_job_slot\", 0)))),\n",
261
+ " \"load_shed_fraction\": float(max(0, min(0.5, action.get(\"load_shed_fraction\", 0.0)))),\n",
262
+ " \"building_id\": 0\n",
263
+ " }\n",
264
+ " r_reset = requests.post(f\"{ENV_URL}/reset\", json={\"task_id\": 1, \"seed\": 42}, timeout=30)\n",
265
+ " if r_reset.status_code != 200:\n",
266
+ " rewards.append(0.0)\n",
267
+ " continue\n",
268
+ " r_step = requests.post(f\"{ENV_URL}/step\", json=[step_action], timeout=30)\n",
269
+ " if r_step.status_code != 200:\n",
270
+ " rewards.append(0.0)\n",
271
+ " continue\n",
272
+ " res = r_step.json().get(\"results\", [{}])[0]\n",
273
+ " step_reward = float(res.get(\"reward\", 0.0))\n",
274
+ " val = (step_reward + 2.0) * 0.08\n",
275
+ " rewards.append(min(0.4, max(0.0, val)))\n",
276
+ " except Exception:\n",
277
+ " rewards.append(0.0)\n",
278
+ " return rewards\n",
279
  "\n",
280
+ "print(\"\u2705 Reward functions defined\")\n",
281
+ "print(\" Total max reward per step: 1.0\")"
282
  ]
283
  },
284
  {
285
  "cell_type": "markdown",
286
  "metadata": {},
287
  "source": [
288
+ "## Step 5 \u2014 Plot Training Curve\n",
289
  "\n",
290
  "This plot is the key **evidence of learning** for the hackathon judges."
291
  ]
 
320
  "plt.tight_layout()\n",
321
  "plt.savefig(\"results/training_curve.png\", dpi=200, bbox_inches='tight')\n",
322
  "plt.show()\n",
323
+ "print(\"\u2705 Training curve saved to results/training_curve.png\")"
324
  ]
325
  },
326
  {
327
  "cell_type": "markdown",
328
  "metadata": {},
329
  "source": [
330
+ "## Step 6 \u2014 Before vs After Comparison\n",
331
  "\n",
332
  "Test the same scenario pre-training and post-training to show qualitative improvement."
333
  ]
 
339
  "outputs": [],
340
  "source": [
341
  "test_state = (\n",
342
+ " \"Building state: temp=24.5\u00b0C (too hot!), price=$0.18/kWh (peak), \"\n",
343
  " \"storage=0.7 (charged), grid_stress=0.85 (CRITICAL!), hour=18, step=60/95\\n\"\n",
344
  " \"Pending batch job deadlines: [12, 30]\\n\"\n",
345
  " \"Cumulative cost so far: $1.24\"\n",
 
362
  " )\n",
363
  "\n",
364
  "response = tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True)\n",
365
+ "print(\"\ud83d\udccb Test Scenario:\")\n",
366
  "print(\" \", test_state.replace(\"\\n\", \"\\n \"))\n",
367
+ "print(\"\\n\ud83e\udd16 Fine-tuned Model Response:\")\n",
368
  "print(\" \", response)\n",
369
+ "print(\"\\n\u2705 Expected: load_shed_fraction > 0 (grid_stress=0.85), thermal_charge_rate < 0 (discharge at peak price)\")"
370
  ]
371
  }
372
  ],