import os import sys import time import subprocess import argparse import numpy as np sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from openenv.client import OpenEnvClient from agent.config import AgentConfig from agent.actor_critic import SAC_Agent def start_server(): import requests _PYROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) # Check if a server is already running (e.g. from Docker or trainer) try: r = requests.get("http://127.0.0.1:8000/api/health", timeout=1) if r.status_code == 200: return None # use existing server silently except Exception: pass log_file = open(os.path.join(os.path.dirname(__file__), "server_eval.log"), "w") env_vars = os.environ.copy() env_vars["PYTHONPATH"] = _PYROOT server_process = subprocess.Popen( [sys.executable, os.path.join(_PYROOT, "backend", "main.py")], env=env_vars, stdout=log_file, stderr=log_file ) # Poll until server is ready (up to 20 seconds) for _ in range(20): time.sleep(1) try: r = requests.get("http://127.0.0.1:8000/api/health", timeout=1) if r.status_code == 200: break except Exception: pass else: print(" WARNING: Server may not be ready. Check server_eval.log for errors.") return server_process def evaluate_model(client, model_path, task, seeds): config = AgentConfig() agent = SAC_Agent(config) # if os.path.exists(model_path + "_actor.pth"): # agent.load(model_path) # print(f" Loaded: {model_path}") # else: # print(f" WARNING: No model at {model_path}. Using random weights.") if os.path.exists(model_path + "_actor.pth"): agent.load(model_path) rewards, soc_at_peak_hrs, peak_violation_rates = [], [], [] cycle_counts, arb_accuracies, fr_scores = [], [], [] for seed in seeds: state = client.reset(seed=seed, task=task) done = False ep_reward = 0 soc_hist, hour_hist = [], [] violations = total_steps = action_dir_correct = 0 fr_score_sum = fr_eligible = direction_changes = 0 prev_soc = None while not done: # Use evaluate=True for deterministic evaluation logic action = np.clip(agent.select_action(np.array(state), evaluate=True), -config.max_action, config.max_action) next_state, reward, terminated, truncated, info = client.step(action) ep_reward += reward total_steps += 1 soc = info["soc"] net_load = info["net_load"] lmp = info["lmp"] r_fr = info["r_fr"] p_avg = float(next_state[3]) if len(next_state) > 3 else lmp hour = int(float(state[0])) soc_hist.append(soc) hour_hist.append(hour) if net_load > 20.0: violations += 1 # Arbitrage direction accuracy price_signal = lmp - p_avg action_final = info["action_final"] if price_signal > 1.0 and action_final < 0: # High price → should discharge action_dir_correct += 1 elif price_signal < -1.0 and action_final > 0: # Low price → should charge action_dir_correct += 1 elif abs(price_signal) <= 1.0: # Neutral zone → any action ok action_dir_correct += 1 if r_fr > 0: fr_score_sum += r_fr fr_eligible += 1 if prev_soc is not None and prev_soc != soc: if (soc > prev_soc) != (prev_soc > 0.5): direction_changes += 1 prev_soc = soc state = next_state done = terminated or truncated rewards.append(ep_reward) peak_violation_rates.append(violations / total_steps * 100) peak_soc = [soc_hist[i] for i, h in enumerate(hour_hist) if 16 <= h <= 20] if peak_soc: soc_at_peak_hrs.append(np.mean(peak_soc)) cycle_counts.append(direction_changes / 2.0) arb_accuracies.append(action_dir_correct / total_steps * 100) fr_scores.append(fr_score_sum / max(fr_eligible, 1)) return { "reward_mean": np.mean(rewards), "reward_std": np.std(rewards), "reward_min": np.min(rewards), "reward_max": np.max(rewards), "soc_at_peak_mean": np.mean(soc_at_peak_hrs) if soc_at_peak_hrs else 0.0, "peak_violation_pct": np.mean(peak_violation_rates), "avg_cycles_per_ep": np.mean(cycle_counts), "arb_accuracy_pct": np.mean(arb_accuracies), "avg_fr_score_per_hit": np.mean(fr_scores), } def score_model(results, task): s = {} ceilings = {"easy": 160000, "medium": 185000, "hard": 190000} def clamp(val): return max(0.001, min(0.999, float(val))) s["reward"] = clamp(results["reward_mean"] / ceilings[task]) s["soc_readiness"] = clamp(results["soc_at_peak_mean"] / 0.75) s["ps_adherence"] = clamp(1.0 - results["peak_violation_pct"] / 20.0) s["cycle_discipline"] = clamp(1.0 - results["avg_cycles_per_ep"] / 200.0) s["arb_accuracy"] = clamp((results["arb_accuracy_pct"] - 50.0) / 50.0) cv = results["reward_std"] / max(abs(results["reward_mean"]), 1) s["consistency"] = clamp(1.0 - cv * 3) if task == "easy": w = {"reward": 0.35, "soc_readiness": 0.25, "ps_adherence": 0.00, "cycle_discipline": 0.15, "arb_accuracy": 0.20, "consistency": 0.05} elif task == "medium": w = {"reward": 0.30, "soc_readiness": 0.20, "ps_adherence": 0.00, "cycle_discipline": 0.15, "arb_accuracy": 0.20, "consistency": 0.15} else: w = {"reward": 0.25, "soc_readiness": 0.15, "ps_adherence": 0.20, "cycle_discipline": 0.15, "arb_accuracy": 0.15, "consistency": 0.10} total = sum(s[k] * w[k] for k in w) return s, total if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--task", type=str, default="all", choices=["easy", "medium", "hard", "all"]) parser.add_argument("--seeds", type=int, default=20, help="Number of evaluation seeds (starting from 300)") args = parser.parse_args() eval_seeds = list(range(300, 300 + args.seeds)) tasks = ["easy", "medium", "hard"] if args.task == "all" else [args.task] print(f"\n{'='*60}") print(f" BESS-RL Evaluation | Seeds {eval_seeds[0]}-{eval_seeds[-1]} (unseen)") print(f"{'='*60}\n") print(" NOTE: Ensure the OpenEnv server is already running:") print(" > uvicorn server.app:app --port 8000") print() client = OpenEnvClient(base_url="http://127.0.0.1:8000") for task in tasks: model_path = os.path.join(os.path.dirname(__file__), "models", f"best_model_{task}") print(f"Evaluating [{task.upper()}] model on {len(eval_seeds)} seeds...") results = evaluate_model(client, model_path, task, eval_seeds) scores, overall = score_model(results, task) print(f"\n --- {task.upper()} Results ---") print(f" Reward: mean={results['reward_mean']:>10.0f} std={results['reward_std']:>8.0f}" f" min={results['reward_min']:>10.0f} max={results['reward_max']:>10.0f}") print(f" SOC at Peak: {results['soc_at_peak_mean']:.1%} (target >70%)") print(f" PS Violations: {results['peak_violation_pct']:.1f}% (target <5%)") print(f" Avg Cycles: {results['avg_cycles_per_ep']:.0f} per episode") print(f" Arb Accuracy: {results['arb_accuracy_pct']:.1f}% (50%=random, 100%=perfect)") print(f"\n --- Dimension Scores ---") for dim, sc in scores.items(): bar = '█' * int(sc * 20) + '░' * (20 - int(sc * 20)) print(f" {dim:<20} [{bar}] {sc:.2f}") print(f"\n ★ OVERALL SCORE: {overall:.3f} / 1.000\n") print(f"{'='*60}\n")