""" Exp 21: Parallel DummyVecEnv — generated_road + generated_track, warm-started. Rationale: - generated_road specialist already exists and drives road markings well. - generated_road and generated_track share the same road semantics. - Background adaptation is the goal here, not mountain physics. Design: - Warm-start from Phase 2 champion (generated_road specialist). - Train in parallel on TWO sim instances: Sim 1: generated_road on port 9091 Sim 2: generated_track on port 9093 - Use the old v4 reward that worked for the flat road tracks. - Keep the wrapper chain minimal: ThrottleClamp + V4 reward only. """ import sys, os, time from collections import deque from datetime import datetime sys.path.insert(0, '/home/paulh/projects/donkeycar-rl-autoresearch/agent') from donkeycar_sb3_runner import ThrottleClampWrapper from multitrack_runner import StuckTerminationWrapper from stable_baselines3 import PPO from stable_baselines3.common.vec_env import DummyVecEnv, VecTransposeImage from stable_baselines3.common.utils import get_schedule_fn import gymnasium as gym import numpy as np HOST = 'localhost' THROTTLE_MIN = 0.2 LR = 0.000225 TOTAL_STEPS = 150_000 CHECKPOINT_EVERY = 10_000 SAVE_DIR = '/home/paulh/projects/donkeycar-rl-autoresearch/agent/models/exp21-generated-pair-warm-v4' WARM_PATH = '/home/paulh/projects/donkeycar-rl-autoresearch/agent/models/champion/model.zip' os.makedirs(SAVE_DIR, exist_ok=True) class V4RewardWrapper(gym.Wrapper): """ v4 reward from the successful flat-road experiments: reward = base_cte * efficiency * (1 + speed_scale * speed) """ def __init__(self, env, speed_scale=0.1, window_size=60, min_efficiency=0.05, max_cte=8.0): super().__init__(env) self.speed_scale = speed_scale self.min_efficiency = min_efficiency self.max_cte = max_cte self._pos_history = deque(maxlen=window_size + 1) def reset(self, **kwargs): self._pos_history.clear() return self.env.reset(**kwargs) def step(self, action): result = self.env.step(action) if len(result) == 5: obs, _sim_reward, terminated, truncated, info = result done = terminated or truncated else: obs, _sim_reward, done, info = result terminated, truncated = done, False reward = self._compute_reward(done, info) if len(result) == 5: return obs, reward, terminated, truncated, info return obs, reward, done, info def _compute_reward(self, done, info): if done: return -1.0 pos = info.get('pos', None) if pos is not None: try: self._pos_history.append(np.array(list(pos)[:3], dtype=np.float64)) except (TypeError, ValueError): pass try: cte = float(info.get('cte', 0.0) or 0.0) except (TypeError, ValueError): cte = 0.0 base = 1.0 - min(abs(cte) / self.max_cte, 1.0) efficiency = self._compute_efficiency() eff = max(0.0, (efficiency - self.min_efficiency) / (1.0 - self.min_efficiency)) try: speed = max(0.0, float(info.get('speed', 0.0) or 0.0)) except (TypeError, ValueError): speed = 0.0 return base * eff * (1.0 + self.speed_scale * speed) def _compute_efficiency(self): if len(self._pos_history) < 3: return 1.0 positions = list(self._pos_history) net = np.linalg.norm(positions[-1] - positions[0]) total = sum( np.linalg.norm(positions[i + 1] - positions[i]) for i in range(len(positions) - 1) ) return float(net / total) if total > 1e-6 else 1.0 def log(msg): print(f'[{datetime.now().strftime("%H:%M:%S")}] {msg}', flush=True) def make_env(track_id, port): def _init(): raw = gym.make(track_id, conf={'host': HOST, 'port': port}) env = ThrottleClampWrapper(raw, throttle_min=THROTTLE_MIN) env = StuckTerminationWrapper( env, stuck_steps=40, min_displacement=0.5, max_stuck_seconds=12.0, max_episode_seconds=30.0, ) env = V4RewardWrapper(env, speed_scale=0.1, window_size=60, min_efficiency=0.05, max_cte=8.0) return env return _init def make_eval_env(track_id, port): inner = make_env(track_id, port)() return VecTransposeImage(DummyVecEnv([lambda e=inner: e])) log('=' * 60) log('Exp 21: generated_road + generated_track, warm-started, v4 reward') log(f' Warm start: {WARM_PATH}') log(f' Sim 1: {HOST}:9091 -> generated_road') log(f' Sim 2: {HOST}:9093 -> generated_track') log(f' throttle_min={THROTTLE_MIN}, lr={LR}, total={TOTAL_STEPS:,}') log(' Termination: StuckTerminationWrapper enabled') log(f' Checkpoints: every {CHECKPOINT_EVERY:,} steps') log('=' * 60) log('Creating DummyVecEnv with the two road tracks...') env = DummyVecEnv([ make_env('donkey-generated-roads-v0', 9091), make_env('donkey-generated-track-v0', 9093), ]) env = VecTransposeImage(env) log(f' VecEnv num_envs={env.num_envs}, obs={env.observation_space.shape}') if not os.path.exists(WARM_PATH): raise FileNotFoundError(WARM_PATH) model = PPO.load(WARM_PATH, env=env, device='cpu') model.learning_rate = LR try: model.lr_schedule = get_schedule_fn(LR) except Exception: model.lr_schedule = None try: for pg in model.policy.optimizer.param_groups: pg['lr'] = LR except Exception: pass log('Warm-start model attached. Starting training...') best_total_steps = float('-inf') best_total_reward = float('-inf') steps_done = 0 while steps_done < TOTAL_STEPS: seg_steps = min(CHECKPOINT_EVERY, TOTAL_STEPS - steps_done) model.learn(total_timesteps=seg_steps, reset_num_timesteps=False) steps_done += seg_steps ckpt = os.path.join(SAVE_DIR, f'checkpoint_{steps_done:07d}') model.save(ckpt) model.save(os.path.join(SAVE_DIR, 'model')) log(f'[{steps_done:,}/{TOTAL_STEPS:,}] Checkpoint saved: {ckpt}.zip') try: obs = env.reset() ep_rewards = np.zeros(env.num_envs) ep_steps = np.zeros(env.num_envs) done_mask = np.zeros(env.num_envs, dtype=bool) for _ in range(2000): action, _ = model.predict(obs, deterministic=True) obs, rewards, dones, infos = env.step(action) for i in range(env.num_envs): if not done_mask[i]: ep_rewards[i] += rewards[i] ep_steps[i] += 1 if dones[i]: done_mask[i] = True if done_mask.all(): break status0 = '✅' if ep_steps[0] >= 2000 else f'❌@{int(ep_steps[0])}' status1 = '✅' if ep_steps[1] >= 2000 else f'❌@{int(ep_steps[1])}' log(f' Eval: gen_road={ep_rewards[0]:.1f}r/{int(ep_steps[0])}s {status0} ' f'gen_track={ep_rewards[1]:.1f}r/{int(ep_steps[1])}s {status1}') total_steps_eval = ep_steps.sum() total_reward = ep_rewards.sum() if (total_steps_eval > best_total_steps or (total_steps_eval == best_total_steps and total_reward > best_total_reward)): best_total_steps = total_steps_eval best_total_reward = total_reward model.save(os.path.join(SAVE_DIR, 'best_model')) log(f' NEW BEST: combined steps={int(best_total_steps)} reward={best_total_reward:.1f}') except Exception as e: log(f' Eval error: {e}') import traceback; traceback.print_exc() model.save(os.path.join(SAVE_DIR, 'model')) log(f'\nTraining complete. Best combined steps: {int(best_total_steps)}') env.close() time.sleep(5) log('\n' + '=' * 60) log('FINAL EVALUATION: best_model on generated_road, generated_track, mini_monaco') log('=' * 60) EVAL_TRACKS = [ ('donkey-generated-roads-v0', 'generated_road'), ('donkey-generated-track-v0', 'generated_track'), ('donkey-minimonaco-track-v0', 'mini_monaco'), ] EVAL_PORT = 9091 EVAL_SETS = 3 EVAL_MAX_STEPS = 2000 best_model_path = os.path.join(SAVE_DIR, 'best_model.zip') results_by_track = {} for track_id, track_name in EVAL_TRACKS: log(f'\n--- {track_name} ---') steps_list = [] for s in range(1, EVAL_SETS + 1): try: eval_env = make_eval_env(track_id, EVAL_PORT) eval_model = PPO.load(best_model_path, env=eval_env, device='cpu') obs = eval_env.reset() total_r, total_s, done = 0.0, 0, False while not done and total_s < EVAL_MAX_STEPS: action, _ = eval_model.predict(obs, deterministic=True) result = eval_env.step(action) if len(result) == 4: obs, r, d, info = result done = bool(d[0]) else: obs, r, t, tr, info = result done = bool(t[0] or tr[0]) total_r += float(r[0]) total_s += 1 status = '✅' if total_s >= EVAL_MAX_STEPS else f'❌@{total_s}' log(f' Set {s}: {total_r:.1f}r / {total_s}s {status}') steps_list.append(total_s) eval_env.close() time.sleep(3) except Exception as e: log(f' Set {s}: ERROR - {e}') steps_list.append(0) time.sleep(3) mean_steps = np.mean(steps_list) if steps_list else 0 results_by_track[track_name] = steps_list log(f' Mean: {mean_steps:.0f} steps') log('\n' + '=' * 60) log('SUMMARY') log('=' * 60) for track_name, steps_list in results_by_track.items(): steps_str = '/'.join(str(s) for s in steps_list) mean = np.mean(steps_list) verdict = '✅' if mean >= 1500 else '⚠️' if mean >= 500 else '❌' log(f' {verdict} {track_name:20s}: {steps_str} mean={mean:.0f}') log('\n=== Exp 21 COMPLETE ===')