Spaces:
Sleeping
Sleeping
File size: 8,077 Bytes
a099b30 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | """
Performance Analytics & Episode History
Tracks episode metrics over time and provides analysis tools.
"""
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
import json
import os
@dataclass
class EpisodeMetrics:
"""Metrics for a single episode."""
episode_id: str
task_id: str
start_time: datetime
end_time: Optional[datetime] = None
steps: int = 0
total_reward: float = 0.0
total_vehicles: int = 0
total_emergency: int = 0
total_waiting_time: float = 0.0
total_collisions: int = 0
phase_changes: int = 0
avg_queue_length: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0, 0.0])
rewards_history: List[float] = field(default_factory=list)
decisions: List[Dict[str, Any]] = field(default_factory=list)
@property
def duration_seconds(self) -> float:
if self.end_time:
return (self.end_time - self.start_time).total_seconds()
return 0.0
@property
def throughput_per_step(self) -> float:
if self.steps > 0:
return self.total_vehicles / self.steps
return 0.0
@property
def avg_reward_per_step(self) -> float:
if self.steps > 0:
return self.total_reward / self.steps
return 0.0
class EpisodeHistory:
"""Store and analyze episode history."""
def __init__(self, max_episodes: int = 100):
self.episodes: List[EpisodeMetrics] = []
self.max_episodes = max_episodes
self._current: Optional[EpisodeMetrics] = None
def start_episode(self, episode_id: str, task_id: str) -> EpisodeMetrics:
"""Start tracking a new episode."""
episode = EpisodeMetrics(
episode_id=episode_id,
task_id=task_id,
start_time=datetime.now(),
)
self._current = episode
return episode
def record_step(
self,
step: int,
reward: float,
action: Dict[str, Any],
observation: Dict[str, Any],
) -> None:
"""Record a step in the current episode."""
if self._current:
self._current.steps = step
self._current.total_reward += reward
self._current.rewards_history.append(reward)
# Track queue lengths for averaging
queues = observation.get("queue_lengths", [0, 0, 0, 0])
for i, q in enumerate(queues):
self._current.avg_queue_length[i] = (
self._current.avg_queue_length[i] * (step - 1) + q
) / step
# Record decision
self._current.decisions.append({
"step": step,
"action": action,
"phase": observation.get("current_phase"),
"queues": queues,
"emergency_queues": observation.get("emergency_queue", [0, 0, 0, 0]),
})
def record_state(self, state: Dict[str, Any]) -> None:
"""Record final state metrics."""
if self._current:
self._current.total_vehicles = state.get("total_vehicles_passed", 0)
self._current.total_emergency = state.get("total_emergency_passed", 0)
self._current.total_waiting_time = state.get("total_waiting_time", 0.0)
self._current.total_collisions = state.get("total_collisions", 0)
self._current.phase_changes = state.get("total_phase_changes", 0)
def end_episode(self) -> EpisodeMetrics:
"""Finalize the current episode."""
if self._current:
self._current.end_time = datetime.now()
self.episodes.append(self._current)
# Trim old episodes
if len(self.episodes) > self.max_episodes:
self.episodes = self.episodes[-self.max_episodes:]
result = self._current
self._current = None
return result
raise RuntimeError("No active episode to end")
def get_summary(self, task_id: Optional[str] = None) -> Dict[str, Any]:
"""Get summary statistics."""
episodes = self.episodes
if task_id:
episodes = [e for e in episodes if e.task_id == task_id]
if not episodes:
return {"message": "No episodes recorded yet"}
total_episodes = len(episodes)
avg_reward = sum(e.avg_reward_per_step for e in episodes) / total_episodes
avg_throughput = sum(e.throughput_per_step for e in episodes) / total_episodes
avg_duration = sum(e.duration_seconds for e in episodes) / total_episodes
# Find best episode
best_idx = max(range(total_episodes), key=lambda i: episodes[i].avg_reward_per_step)
best = episodes[best_idx]
return {
"total_episodes": total_episodes,
"avg_reward_per_step": round(avg_reward, 4),
"avg_throughput_per_step": round(avg_throughput, 4),
"avg_duration_seconds": round(avg_duration, 2),
"best_episode": {
"episode_id": best.episode_id,
"task_id": best.task_id,
"reward_per_step": round(best.avg_reward_per_step, 4),
"total_reward": round(best.total_reward, 2),
"steps": best.steps,
},
"recent_performance": [
{
"episode_id": e.episode_id,
"task_id": e.task_id,
"reward_per_step": round(e.avg_reward_per_step, 4),
"steps": e.steps,
}
for e in episodes[-10:]
],
}
def get_episode_details(self, episode_id: str) -> Optional[Dict[str, Any]]:
"""Get detailed metrics for a specific episode."""
for episode in self.episodes:
if episode.episode_id == episode_id:
return {
"episode_id": episode.episode_id,
"task_id": episode.task_id,
"start_time": episode.start_time.isoformat(),
"end_time": episode.end_time.isoformat() if episode.end_time else None,
"duration_seconds": episode.duration_seconds,
"steps": episode.steps,
"total_reward": round(episode.total_reward, 2),
"total_vehicles": episode.total_vehicles,
"total_emergency": episode.total_emergency,
"total_collisions": episode.total_collisions,
"phase_changes": episode.phase_changes,
"avg_queue_lengths": [round(q, 2) for q in episode.avg_queue_length],
"throughput_per_step": round(episode.throughput_per_step, 4),
"reward_per_step": round(episode.avg_reward_per_step, 4),
"rewards_history": [round(r, 3) for r in episode.rewards_history],
"decision_count": len(episode.decisions),
}
return None
def export_to_json(self, filepath: str) -> None:
"""Export all episodes to JSON file."""
data = {
"export_time": datetime.now().isoformat(),
"total_episodes": len(self.episodes),
"episodes": [
{
"episode_id": e.episode_id,
"task_id": e.task_id,
"steps": e.steps,
"total_reward": e.total_reward,
"total_vehicles": e.total_vehicles,
"avg_reward_per_step": e.avg_reward_per_step,
}
for e in self.episodes
],
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
# Global history instance
_episode_history = EpisodeHistory()
def get_history() -> EpisodeHistory:
"""Get the global episode history instance."""
return _episode_history
|