demeter

Autonomous Hydroponic Intelligence

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()