"""Evaluation & comparison: DiscoRL vs SB3 PPO on CartPole. This script: 1. Trains a standard SB3 PPO agent on CartPole (baseline) 2. Evaluates the DiscoRL-trained agent 3. Compares performance metrics 4. Provides visualization / reporting """ import os import sys # Force JAX to use CPU only (avoid GPU memory issues) os.environ['JAX_PLATFORMS'] = 'cpu' import numpy as np import matplotlib.pyplot as plt from typing import Dict, List, Tuple # Ensure imports work current_dir = os.path.dirname(os.path.abspath(__file__)) repo_root = os.path.abspath(os.path.join(current_dir, os.pardir)) sys.path.insert(0, os.path.join(repo_root, 'disco_rl')) import gymnasium as gym from stable_baselines3 import PPO from stable_baselines3.common.env_util import make_vec_env from disco_cartpole_env import CartPoleDiscoWrapper, DiscoCartPoleEnv import jax import jax.numpy as jnp from disco_rl import agent as disco_agent from disco_weights import load_disco103_weights def train_sb3_ppo( total_timesteps: int = 50000, n_steps: int = 2048, batch_size: int = 64, learning_rate: float = 3e-4, ) -> PPO: """Train SB3 PPO agent on CartPole. Returns: trained PPO model """ print('\n' + '='*60) print('Training SB3 PPO Baseline') print('='*60) env = make_vec_env('CartPole-v1', n_envs=4) model = PPO( 'MlpPolicy', env, n_steps=n_steps, batch_size=batch_size, learning_rate=learning_rate, verbose=1, device='cpu', # or 'cuda:0' if GPU available ) model.learn(total_timesteps=total_timesteps) env.close() print('SB3 PPO training complete') return model def evaluate_sb3_ppo(model: PPO, num_episodes: int = 10) -> Dict[str, float]: """Evaluate trained SB3 PPO. Returns: dict with 'mean_reward', 'std_reward', etc. """ env = gym.make('CartPole-v1') episode_rewards = [] episode_lengths = [] for ep in range(num_episodes): obs, _ = env.reset() done = False ep_reward = 0 ep_len = 0 while not done and ep_len < 500: action, _ = model.predict(obs, deterministic=True) obs, reward, terminated, truncated, info = env.step(action) done = terminated or truncated ep_reward += reward ep_len += 1 episode_rewards.append(ep_reward) episode_lengths.append(ep_len) env.close() results = { 'mean_reward': float(np.mean(episode_rewards)), 'std_reward': float(np.std(episode_rewards)), 'mean_length': float(np.mean(episode_lengths)), 'std_length': float(np.std(episode_lengths)), 'min_reward': float(np.min(episode_rewards)), 'max_reward': float(np.max(episode_rewards)), } print(f' Mean reward: {results["mean_reward"]:.1f} ± {results["std_reward"]:.1f}') print(f' Mean length: {results["mean_length"]:.1f} ± {results["std_length"]:.1f}') print(f' Min/Max reward: {results["min_reward"]:.1f} / {results["max_reward"]:.1f}') return results def evaluate_disco_agent( agent_params: Dict, update_rule_params: Dict, num_episodes: int = 10, ) -> Dict[str, float]: """Evaluate trained DiscoRL agent. Returns: dict with 'mean_reward', 'std_reward', etc. """ # Create env and agent env = DiscoCartPoleEnv(batch_size=1) agent_settings = disco_agent.get_settings_disco() agent = disco_agent.Agent( single_observation_spec=env.single_observation_spec(), single_action_spec=env.single_action_spec(), agent_settings=agent_settings, batch_axis_name=None, ) # Initialize actor state (same across episodes) rng = jax.random.PRNGKey(0) actor_state_template = agent.initial_actor_state(rng) episode_rewards = [] episode_lengths = [] for ep in range(num_episodes): _, env_t = env.reset() rng, subkey = jax.random.split(rng) actor_state = actor_state_template ep_reward = 0.0 ep_len = 0 done = False while not done and ep_len < 500: rng, subkey = jax.random.split(rng) actor_timestep, actor_state = agent.actor_step( agent_params, subkey, env_t, actor_state, ) action = np.asarray(actor_timestep.actions)[0] _, env_t = env.step(None, [action]) done = bool(np.asarray(env_t.step_type)[0] == 1) reward = float(np.asarray(env_t.reward)[0]) ep_reward += reward ep_len += 1 episode_rewards.append(ep_reward) episode_lengths.append(ep_len) results = { 'mean_reward': float(np.mean(episode_rewards)), 'std_reward': float(np.std(episode_rewards)), 'mean_length': float(np.mean(episode_lengths)), 'std_length': float(np.std(episode_lengths)), 'min_reward': float(np.min(episode_rewards)), 'max_reward': float(np.max(episode_rewards)), } print(f' Mean reward: {results["mean_reward"]:.1f} ± {results["std_reward"]:.1f}') print(f' Mean length: {results["mean_length"]:.1f} ± {results["std_length"]:.1f}') print(f' Min/Max reward: {results["min_reward"]:.1f} / {results["max_reward"]:.1f}') return results def plot_comparison(sb3_results: Dict, disco_results: Dict, save_path: str = None): """Plot comparison results.""" fig, axes = plt.subplots(1, 2, figsize=(12, 4)) methods = ['SB3 PPO', 'DiscoRL'] means = [sb3_results['mean_reward'], disco_results['mean_reward']] stds = [sb3_results['std_reward'], disco_results['std_reward']] # Reward plot axes[0].bar(methods, means, yerr=stds, capsize=5, alpha=0.7, color=['blue', 'orange']) axes[0].set_ylabel('Mean Episode Reward') axes[0].set_title('Episode Reward Comparison') axes[0].grid(True, alpha=0.3) # Length plot lengths = [sb3_results['mean_length'], disco_results['mean_length']] length_stds = [sb3_results['std_length'], disco_results['std_length']] axes[1].bar(methods, lengths, yerr=length_stds, capsize=5, alpha=0.7, color=['blue', 'orange']) axes[1].set_ylabel('Mean Episode Length') axes[1].set_title('Episode Length Comparison') axes[1].grid(True, alpha=0.3) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=100, bbox_inches='tight') print(f'\nSaved comparison plot to {save_path}') else: plt.show() def main(): print('='*60) print('DiscoRL vs SB3 PPO on CartPole') print('='*60) # Train SB3 baseline print('\n[1/4] Training SB3 PPO...') sb3_model = train_sb3_ppo(total_timesteps=50000) # Evaluate SB3 print('\n[2/4] Evaluating SB3 PPO...') sb3_results = evaluate_sb3_ppo(num_episodes=20) # Try to load DiscoRL agent from checkpoint print('\n[3/4] Loading DiscoRL agent...') checkpoint_path = os.path.join(repo_root, 'models', 'disco_cartpole', 'final_agent.npz') if os.path.exists(checkpoint_path): print(f' Found checkpoint at {checkpoint_path}') data = np.load(checkpoint_path, allow_pickle=True) agent_params = jax.tree.map(jnp.asarray, data['params'].item()) else: print(f' Checkpoint not found at {checkpoint_path}') print(' Using randomly initialized agent params (will not be competitive).') env = DiscoCartPoleEnv(batch_size=1) agent_settings = disco_agent.get_settings_disco() agent = disco_agent.Agent( single_observation_spec=env.single_observation_spec(), single_action_spec=env.single_action_spec(), agent_settings=agent_settings, batch_axis_name=None, ) rng = jax.random.PRNGKey(0) learner_state = agent.initial_learner_state(rng) agent_params = learner_state.params # Load meta params try: disco_weights = load_disco103_weights( disco_rl_path=os.path.join(repo_root, 'disco_rl') ) update_rule_params = disco_weights except FileNotFoundError: print(' Warning: Could not load Disco103 weights; using random initialization.') update_rule_params = None # Evaluate DiscoRL print('\n[4/4] Evaluating DiscoRL agent...') disco_results = evaluate_disco_agent(agent_params, update_rule_params, num_episodes=20) # Comparison summary print('\n' + '='*60) print('Comparison Summary') print('='*60) print(f'{"Method":<15} {"Mean Reward":<20} {"Mean Length":<20}') print('-'*55) print(f'{"SB3 PPO":<15} {sb3_results["mean_reward"]:<20.1f} {sb3_results["mean_length"]:<20.1f}') print(f'{"DiscoRL":<15} {disco_results["mean_reward"]:<20.1f} {disco_results["mean_length"]:<20.1f}') # Plot comparison plot_save_path = os.path.join(repo_root, 'output', 'disco_vs_sb3_comparison.png') os.makedirs(os.path.dirname(plot_save_path), exist_ok=True) plot_comparison(sb3_results, disco_results, save_path=plot_save_path) if __name__ == '__main__': main()