demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
agent/Marl/train-bandit.py (9498B)
1import sys
2import os
3import numpy as np
4import random
5import time
6
7# 1. Setup Path to import sibling modules
8current_dir = os.path.dirname(os.path.abspath(__file__))
9parent_dir = os.path.dirname(os.path.dirname(current_dir))
10sys.path.append(parent_dir)
11
12from agent.Marl.bandit import ContextualBandit
13from agent.Marl.strategies import STRATEGIES, NUM_ACTIONS
14
15def generate_scenario():
16 """
17 Generates DISTINCT scenarios with clear separations between strategies.
18 Returns:
19 context (np.array): 515-dim vector (512 visual + 3 sensors)
20 sensors (dict): Sensor readings
21 target_strategy (int): The index of the correct strategy
22 scenario_name (str): Description for logging
23 """
24 scenario_type = random.randint(0, NUM_ACTIONS - 1)
25
26 # Defaults - use float32 for consistency and speed
27 vis_vec = np.zeros(512, dtype=np.float32)
28 ph = random.uniform(5.8, 6.5)
29 ec = random.uniform(1.2, 1.8)
30 temp = random.uniform(22.0, 26.0)
31
32 target_strategy = 0
33 name = "Normal"
34
35 # --- 0. MAINTAIN_CURRENT (FIX: Very obvious optimal conditions) ---
36 if scenario_type == 0:
37 ph = random.uniform(5.9, 6.3) # Perfect pH
38 ec = random.uniform(1.3, 1.7) # Perfect EC
39 temp = random.uniform(23.0, 25.0) # Perfect temp
40 # Minimal visual noise (healthy plant)
41 vis_vec[0:5] = np.random.uniform(0.1, 0.3, 5)
42 name = "Optimal Conditions"
43 target_strategy = 0
44
45 # --- 1. CALIBRATE_SENSORS (FIX: Physically impossible values) ---
46 elif scenario_type == 1:
47 choice = random.randint(0, 2)
48 if choice == 0:
49 ph = random.choice([-10.0, -5.0, 0.0, 15.0, 20.0, 50.0])
50 elif choice == 1:
51 ec = random.choice([-10.0, 0.0, 50.0, 100.0, 500.0])
52 else:
53 temp = random.choice([-50.0, -20.0, 100.0, 150.0, 500.0])
54
55 # Strong visual anomaly signal
56 vis_vec[0:20] = np.random.uniform(0.9, 1.0, 20)
57 name = "Sensor Malfunction"
58 target_strategy = 1
59
60 # --- 2. AGGRESSIVE_PH_DOWN ---
61 elif scenario_type == 2:
62 ph = random.uniform(7.8, 12.0) # Very alkaline
63 name = "High pH (Alkaline)"
64 target_strategy = 2
65
66 # --- 3. AGGRESSIVE_PH_UP ---
67 elif scenario_type == 3:
68 ph = random.uniform(0.5, 4.2) # Very acidic
69 name = "Low pH (Acidic)"
70 target_strategy = 3
71
72 # --- 4. GENTLE_PH_BALANCING (FIX: Clear mild range) ---
73 elif scenario_type == 4:
74 if random.random() < 0.5:
75 ph = random.uniform(5.4, 5.8) # Slightly low
76 else:
77 ph = random.uniform(6.4, 6.9) # Slightly high
78 # Keep other sensors perfect
79 ec = random.uniform(1.4, 1.6)
80 temp = random.uniform(23.5, 24.5)
81 name = "Mild pH Drift"
82 target_strategy = 4
83
84 # --- 5. INCREASE_EC_VEG ---
85 elif scenario_type == 5:
86 ec = random.uniform(0.1, 0.9) # Low EC
87 # Strong vegetative signal (first block)
88 vis_vec[20:40] = np.random.uniform(0.6, 1.0, 20)
89 name = "Low EC (Veg Stage)"
90 target_strategy = 5
91
92 # --- 6. INCREASE_EC_BLOOM ---
93 elif scenario_type == 6:
94 ec = random.uniform(0.1, 0.9) # Low EC
95 # Strong flowering signal (second block)
96 vis_vec[40:60] = np.random.uniform(0.6, 1.0, 20)
97 name = "Low EC (Bloom Stage)"
98 target_strategy = 6
99
100 # --- 7. LOWER_EC_FLUSH ---
101 elif scenario_type == 7:
102 ec = random.uniform(2.8, 8.0) # Very high EC
103 # Nutrient burn visual (third block)
104 vis_vec[60:80] = np.random.uniform(0.7, 1.0, 20)
105 name = "Nutrient Burn / High EC"
106 target_strategy = 7
107
108 # --- 8. CALMAG_BOOST ---
109 elif scenario_type == 8:
110 # CalMag deficiency visual pattern (fourth block)
111 vis_vec[80:100] = np.random.uniform(0.8, 1.0, 20)
112 name = "CalMag Deficiency (Visual)"
113 target_strategy = 8
114
115 # --- 9. RAISE_TEMP_HUMIDITY ---
116 elif scenario_type == 9:
117 temp = random.uniform(8.0, 17.0) # Too cold
118 name = "Too Cold / Low VPD"
119 target_strategy = 9
120
121 # --- 10. LOWER_TEMP_HUMIDITY ---
122 elif scenario_type == 10:
123 temp = random.uniform(32.0, 50.0) # Too hot
124 name = "Too Hot / High VPD"
125 target_strategy = 10
126
127 # --- 11. MAX_AIR_CIRCULATION ---
128 elif scenario_type == 11:
129 # Stagnant air visual (fifth block)
130 vis_vec[100:120] = np.random.uniform(0.6, 1.0, 20)
131 name = "Stagnant Air / Weak Stems"
132 target_strategy = 11
133
134 # --- 12. FUNGAL_TREATMENT ---
135 elif scenario_type == 12:
136 # Fungal infection visual (sixth block)
137 vis_vec[120:140] = np.random.uniform(0.8, 1.0, 20)
138 temp = random.uniform(26.0, 32.0) # Warm and humid conditions
139 name = "Fungal Infection Detected"
140 target_strategy = 12
141
142 # --- 13. PEST_ISOLATION ---
143 elif scenario_type == 13:
144 # Pest visual (seventh block)
145 vis_vec[140:160] = np.random.uniform(0.8, 1.0, 20)
146 name = "Pest Infestation Detected"
147 target_strategy = 13
148
149 # --- 14. PRUNE_NECROTIC_LEAVES ---
150 elif scenario_type == 14:
151 # Necrosis visual (eighth block)
152 vis_vec[160:180] = np.random.uniform(0.8, 1.0, 20)
153 name = "Necrosis Detected"
154 target_strategy = 14
155
156 # Build Final Context Vector
157 sensor_vec = np.array([
158 (ph - 6.0) / 2.0,
159 (ec - 1.0) / 3.0,
160 (temp - 25.0) / 40.0
161 ], dtype=np.float32)
162
163 context = np.concatenate([vis_vec, sensor_vec])
164
165 return context, {'pH': ph, 'EC': ec, 'temp': temp}, target_strategy, name
166
167def train():
168 print("=" * 80)
169 print("🧠 CONTEXTUAL BANDIT TRAINING - HYDROPONIC CONTROL SYSTEM")
170 print("=" * 80)
171 print(f" Strategy Count: {NUM_ACTIONS}")
172 print(f" Feature Dimensions: 515 (512 Vision + 3 Sensors)")
173 print(f" Training Episodes: 15,000")
174 print()
175
176 bandit = ContextualBandit(n_actions=NUM_ACTIONS, feature_dim=515)
177
178 n_epochs = 15000
179 correct_counts = np.zeros(NUM_ACTIONS, dtype=int)
180 total_counts = np.zeros(NUM_ACTIONS, dtype=int)
181
182 start_time = time.time()
183 last_print_time = start_time
184
185 for i in range(n_epochs):
186 # 1. Generate scenario
187 context, sensors, target_action, scenario_name = generate_scenario()
188
189 # 2. Bandit prediction
190 chosen_action_idx, debug_info = bandit.select_action(context)
191
192 # 3. Calculate reward
193 if chosen_action_idx == target_action:
194 reward = 1.0
195 correct_counts[target_action] += 1
196 else:
197 reward = -1.0
198
199 total_counts[target_action] += 1
200
201 # 4. Update bandit
202 bandit.update(context, chosen_action_idx, reward)
203
204 # Progress logging (every 2 seconds or every 1000 steps)
205 current_time = time.time()
206 if (i + 1) % 1000 == 0 or (current_time - last_print_time >= 2.0):
207 elapsed = current_time - start_time
208 speed = (i + 1) / elapsed
209 eta = (n_epochs - i - 1) / speed if speed > 0 else 0
210 current_acc = correct_counts.sum() / total_counts.sum() * 100 if total_counts.sum() > 0 else 0
211
212 print(f" [{i+1:5}/{n_epochs}] "
213 f"Accuracy: {current_acc:5.1f}% | "
214 f"Speed: {speed:4.0f} it/s | "
215 f"ETA: {eta:4.0f}s | "
216 f"Last: {scenario_name[:25]:<25}")
217 last_print_time = current_time
218
219 end_time = time.time()
220 training_time = end_time - start_time
221
222 print(f"\n✅ Training Complete in {training_time:.1f}s ({training_time/60:.1f} min)")
223 print(f" Average Speed: {n_epochs/training_time:.0f} iterations/second")
224
225 # Detailed accuracy report
226 print("\n" + "=" * 80)
227 print("📊 FINAL ACCURACY REPORT BY STRATEGY")
228 print("=" * 80)
229 print(f"{'STRATEGY':<30} | {'ACCURACY':>10} | {'CORRECT':>8} / {'TOTAL':>8}")
230 print("-" * 80)
231
232 overall_correct = correct_counts.sum()
233 overall_total = total_counts.sum()
234
235 # Sort by accuracy (worst first) to highlight problems
236 strategy_performance = []
237 for idx in range(NUM_ACTIONS):
238 count = total_counts[idx]
239 correct = correct_counts[idx]
240 acc = (correct / count) * 100 if count > 0 else 0.0
241 strategy_performance.append((idx, acc, correct, count))
242
243 strategy_performance.sort(key=lambda x: x[1]) # Sort by accuracy
244
245 for idx, acc, correct, count in strategy_performance:
246 status = "✅" if acc >= 90 else "⚠️" if acc >= 70 else "❌"
247 print(f"{status} {STRATEGIES[idx]:<27} | {acc:>9.1f}% | {correct:>8} / {count:>8}")
248
249 print("-" * 80)
250 overall_acc = (overall_correct / overall_total) * 100
251 print(f"{'OVERALL ACCURACY':<30} | {overall_acc:>9.1f}% | {overall_correct:>8} / {overall_total:>8}")
252 print("=" * 80 + "\n")
253
254 # Save model
255 bandit.save()
256
257 # Final recommendations
258 if overall_acc >= 95:
259 print("🎉 Excellent! Model ready for production.")
260 elif overall_acc >= 90:
261 print("✅ Good! Model is ready.")
262 elif overall_acc >= 80:
263 print("⚠️ Acceptable, but consider retraining with more epochs.")
264 else:
265 print("❌ Low accuracy. Check scenario generation or retrain.")
266
267if __name__ == "__main__":
268 train()