#!/usr/bin/env python3 """Test script to verify DiscoRL environment fix. This script tests that: 1. Environment properly handles episode boundaries 2. Rewards are correctly propagated 3. Episode lengths are reasonable (not always 1) """ import os import sys import numpy as np import jax import jax.numpy as jnp # Setup paths 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')) from disco_cartpole_env import DiscoCartPoleEnv def test_environment_basic(): """Test basic environment functionality.""" print('=' * 70) print('TEST 1: Basic Environment Functionality') print('=' * 70) env = DiscoCartPoleEnv(batch_size=2, max_steps=50) # Reset _, timestep = env.reset() print(f'✓ Reset successful') print(f' Observation shape: {timestep.observation["observation"].shape}') print(f' Reward shape: {timestep.reward.shape}') print(f' Step type shape: {timestep.step_type.shape}') # Step actions = np.array([0, 1]) # batch of 2 actions _, timestep = env.step(None, actions) print(f'✓ Step successful') print(f' Rewards: {timestep.reward}') print(f' Step types: {timestep.step_type}') return env def test_episode_boundaries(): """Test that episode boundaries are handled correctly.""" print('\n' + '=' * 70) print('TEST 2: Episode Boundary Handling') print('=' * 70) env = DiscoCartPoleEnv(batch_size=1, max_steps=500) _, timestep = env.reset() episode_lengths = [] current_length = 0 max_episodes = 5 episodes_completed = 0 step_count = 0 max_total_steps = 5000 np.random.seed(42) while episodes_completed < max_episodes and step_count < max_total_steps: # Random actions (gives better results than constant action) actions = np.array([np.random.randint(0, 2)]) _, timestep = env.step(None, actions) step_count += 1 current_length += 1 # Check for episode boundary if timestep.step_type[0] == 2: # LAST episodes_completed += 1 episode_lengths.append(current_length) print(f'Episode {episodes_completed} finished: length={current_length}') # Reset environment for next episode _, timestep = env.reset() current_length = 0 # Verify episode lengths print(f'\n✓ Episodes completed: {episodes_completed}') print(f' Episode lengths: {episode_lengths}') # Check that not all episodes are length 1 min_length = min(episode_lengths) max_length = max(episode_lengths) avg_length = np.mean(episode_lengths) print(f' Min length: {min_length}') print(f' Max length: {max_length}') print(f' Avg length: {avg_length:.1f}') if max_length <= 1: print('❌ FAILED: All episodes are length 1 or less (fix not working)') return False else: print('✅ PASSED: Episodes have varied lengths > 1') return True def test_reward_propagation(): """Test that rewards are correctly propagated.""" print('\n' + '=' * 70) print('TEST 3: Reward Propagation') print('=' * 70) env = DiscoCartPoleEnv(batch_size=1, max_steps=100) _, timestep = env.reset() all_rewards = [] num_steps = 20 for step in range(num_steps): actions = np.array([0]) # Action: push left _, timestep = env.step(None, actions) all_rewards.append(float(timestep.reward[0])) print(f'✓ Collected {num_steps} steps') print(f' Reward sequence: {all_rewards}') print(f' Min reward: {min(all_rewards)}') print(f' Max reward: {max(all_rewards)}') print(f' Sum reward: {sum(all_rewards)}') # CartPole gives reward of 1.0 for each step before terminal expected_rewards = [1.0] * num_steps # or until terminal # Check that rewards are 1.0 for non-terminal steps non_terminal_rewards = [r for r in all_rewards if r > 0] if not non_terminal_rewards: print('❌ FAILED: No non-zero rewards (fix not working)') return False if all(r == 1.0 for r in non_terminal_rewards): print('✅ PASSED: Non-terminal rewards are correctly 1.0') return True else: print('⚠️ WARNING: Unexpected reward values') return False def test_batch_handling(): """Test that batch environments work correctly.""" print('\n' + '=' * 70) print('TEST 4: Batch Environment Handling') print('=' * 70) batch_size = 4 env = DiscoCartPoleEnv(batch_size=batch_size, max_steps=50) _, timestep = env.reset() print(f'✓ Created batch environment with size {batch_size}') # Run multiple steps for step in range(10): actions = np.array([np.random.randint(0, 2) for _ in range(batch_size)]) _, timestep = env.step(None, actions) print(f'✓ Successfully stepped through {10} batched steps') print(f' Observation shape: {timestep.observation["observation"].shape}') print(f' Rewards shape: {timestep.reward.shape}') print(f' Expected: ({batch_size},)') print(f' Actual: {timestep.reward.shape}') if timestep.reward.shape == (batch_size,): print('✅ PASSED: Batch shapes are correct') return True else: print('❌ FAILED: Batch shapes mismatch') return False def test_episode_done_tracking(): """Test that _episode_done flag works correctly.""" print('\n' + '=' * 70) print('TEST 5: Episode Done Flag Tracking') print('=' * 70) env = DiscoCartPoleEnv(batch_size=2, max_steps=10) _, timestep = env.reset() print(f'Initial _episode_done: {env._episode_done}') print(f'Initial _step_counts: {env._step_counts}') # Step until first episode ends step_num = 0 for _ in range(100): actions = np.array([1, 0]) # Different actions for batch _, timestep = env.step(None, actions) step_num += 1 if np.any(timestep.step_type == 1): # Any episode done print(f'\nAfter {step_num} steps:') print(f' Step types: {timestep.step_type}') print(f' _episode_done: {env._episode_done}') print(f' _step_counts: {env._step_counts}') print(f'✅ PASSED: Episode done flags tracked correctly') return True print('⚠️ WARNING: No episodes completed (may be fine depending on randomness)') return True def main(): print('\n') print('█' * 70) print(' DiscoRL Environment Fix Verification Tests') print('█' * 70) print() results = [] # Test 1 try: test_environment_basic() results.append(('Basic Functionality', True)) except Exception as e: print(f'❌ FAILED: {e}') results.append(('Basic Functionality', False)) # Test 2 try: passed = test_episode_boundaries() results.append(('Episode Boundaries', passed)) except Exception as e: print(f'❌ FAILED: {e}') results.append(('Episode Boundaries', False)) # Test 3 try: passed = test_reward_propagation() results.append(('Reward Propagation', passed)) except Exception as e: print(f'❌ FAILED: {e}') results.append(('Reward Propagation', False)) # Test 4 try: passed = test_batch_handling() results.append(('Batch Handling', passed)) except Exception as e: print(f'❌ FAILED: {e}') results.append(('Batch Handling', False)) # Test 5 try: passed = test_episode_done_tracking() results.append(('Episode Done Tracking', passed)) except Exception as e: print(f'❌ FAILED: {e}') results.append(('Episode Done Tracking', False)) # Summary print('\n' + '=' * 70) print('TEST SUMMARY') print('=' * 70) for test_name, passed in results: status = '✅ PASS' if passed else '❌ FAIL' print(f'{status}: {test_name}') total_passed = sum(1 for _, p in results if p) total_tests = len(results) print(f'\nTotal: {total_passed}/{total_tests} tests passed') if total_passed == total_tests: print('\n🎉 ALL TESTS PASSED! Fix is working correctly.') return 0 else: print(f'\n⚠️ {total_tests - total_passed} test(s) failed. Please review.') return 1 if __name__ == '__main__': exit_code = main() sys.exit(exit_code)