| """ |
| drone_env/rl/trainer.py |
| # Experience replay and training logic for the Drone Delivery Env. |
| """ |
| import json |
| import os |
| import torch |
| from typing import List, Dict, Any |
| from pathlib import Path |
|
|
| MEMORY_PATH = Path("data/memory.json") |
|
|
|
|
| def record_episode( |
| task: str, |
| steps: List[Dict], |
| grid_meta: Dict, |
| delivery_positions: List[List[int]], |
| deliveries_done: int, |
| total_reward: float |
| ): |
| """ |
| Persist an episode's path history and metadata to memory.json. |
| """ |
| if not os.path.exists("data"): |
| os.makedirs("data") |
| |
| episodes = [] |
| if MEMORY_PATH.exists(): |
| try: |
| with open(MEMORY_PATH, "r") as f: |
| episodes = json.load(f) |
| except (json.JSONDecodeError, ValueError): |
| episodes = [] |
| |
| episode_data = { |
| "task": task, |
| "steps": steps, |
| "grid_meta": grid_meta, |
| "delivery_positions": delivery_positions, |
| "deliveries_done": deliveries_done, |
| "total_reward": total_reward, |
| "total_steps": len(steps) |
| } |
| |
| episodes.append(episode_data) |
| |
| |
| if len(episodes) > 100: |
| episodes = episodes[-100:] |
| |
| with open(MEMORY_PATH, "w") as f: |
| json.dump(episodes, f, indent=2) |
|
|
|
|
| class PathLearner: |
| """ |
| Analyzes episode data stored in memory.json. |
| """ |
| |
| @staticmethod |
| def analyse_episodes(task_name: str): |
| """ |
| Computes statistics for the specified task from memory.json. |
| """ |
| if not MEMORY_PATH.exists(): |
| return {"status": "No data", "message": "Collect data first!"} |
| |
| try: |
| with open(MEMORY_PATH, "r") as f: |
| episodes = json.load(f) |
| except (json.JSONDecodeError, ValueError): |
| return {"status": "Error", "message": "Invalid memory file."} |
| |
| |
| task_episodes = [e for e in episodes if e["task"] == task_name] |
| |
| if not task_episodes: |
| return {"status": "No data", "message": f"No episodes for {task_name}"} |
| |
| total_ep = len(task_episodes) |
| |
| |
| avg_reward = sum(max(0.01, min(0.99, e.get("total_reward", 0.0))) for e in task_episodes) / total_ep |
| avg_steps = sum(e.get("total_steps", 0) for e in task_episodes) / total_ep |
| avg_del = sum(e.get("deliveries_done", 0) for e in task_episodes) / total_ep |
| |
| |
| action_counts = {"UP": 0, "DOWN": 0, "LEFT": 0, "RIGHT": 0, "WAIT": 0} |
| for ep in task_episodes: |
| for step in ep["steps"]: |
| act = step.get("action", "WAIT").upper() |
| if act in action_counts: |
| action_counts[act] += 1 |
| |
| return { |
| "status": "Success", |
| "total_episodes": total_ep, |
| "avg_reward": float(max(0.01, min(0.99, round(avg_reward, 3)))), |
| "avg_steps": round(avg_steps, 1), |
| "avg_deliveries": f"{avg_del:.1f}", |
| "action_distribution": action_counts |
| } |
|
|
|
|
| |
|
|
| from .model import PathQNet, CELL2IDX, ACTIONS |
|
|
| def get_action_from_policy(obs: Any, task_name: str = "easy_delivery") -> str: |
| """ |
| Autonomous mode: Predict next move using trained PyTorch model for the specific task. |
| """ |
| task_short = task_name.split('_')[0] |
| model_path = f"data/{task_short}.pth" |
|
|
| try: |
| if not os.path.exists(model_path): |
| |
| if obs.current_target: |
| tx, ty = obs.current_target |
| dx, dy = tx - obs.drone_x, ty - obs.drone_y |
| |
| |
| if abs(dx) > abs(dy): |
| if dx > 0 and obs.drone_x < obs.grid_width - 1: return "RIGHT" |
| if dx < 0 and obs.drone_x > 0: return "LEFT" |
| if dy > 0 and obs.drone_y < obs.grid_height - 1: return "DOWN" |
| if dy < 0 and obs.drone_y > 0: return "UP" |
| else: |
| if dy > 0 and obs.drone_y < obs.grid_height - 1: return "DOWN" |
| if dy < 0 and obs.drone_y > 0: return "UP" |
| if dx > 0 and obs.drone_x < obs.grid_width - 1: return "RIGHT" |
| if dx < 0 and obs.drone_x > 0: return "LEFT" |
| |
| return "WAIT" |
| return "WAIT" |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model = PathQNet(embed_dim=64).to(device) |
| model.load_state_dict(torch.load(model_path, map_location=device, weights_only=True)) |
| model.eval() |
| |
| |
| grid = obs.cell_types |
| flat_grid = [] |
| for row in grid: |
| for cell in row: |
| flat_grid.append(CELL2IDX.get(cell, 0)) |
| |
| g_t = torch.tensor([flat_grid], dtype=torch.long, device=device) |
| d_t = torch.tensor([[obs.drone_x/obs.grid_width, obs.drone_y/obs.grid_height]], dtype=torch.float, device=device) |
| b_t = torch.tensor([[obs.battery]], dtype=torch.float, device=device) |
| |
| if obs.current_target: |
| tx, ty = obs.current_target |
| t_t = torch.tensor([[tx/obs.grid_width, ty/obs.grid_height]], dtype=torch.float, device=device) |
| else: |
| t_t = torch.tensor([[0.0, 0.0]], dtype=torch.float, device=device) |
|
|
| with torch.no_grad(): |
| q_values = model(g_t, d_t, b_t, t_t) |
| action_idx = q_values.argmax().item() |
| action_str = ACTIONS[action_idx] |
| |
| |
| if action_str == "LEFT" and obs.drone_x <= 0: action_str = "WAIT" |
| if action_str == "RIGHT" and obs.drone_x >= obs.grid_width - 1: action_str = "WAIT" |
| if action_str == "UP" and obs.drone_y <= 0: action_str = "WAIT" |
| if action_str == "DOWN" and obs.drone_y >= obs.grid_height - 1: action_str = "WAIT" |
| |
| return action_str |
| |
| except Exception as e: |
| print(f"[Trainer] Inference Error: {e}") |
| return "WAIT" |
|
|