| """ |
| Visualization utilities for FSD model outputs. |
| Generates visual representations of: |
| - Sensor placement on vehicle |
| - BEV perception outputs |
| - Planned trajectory |
| - Control commands |
| """ |
|
|
| import torch |
| import numpy as np |
| from typing import Dict, Optional, Tuple, List |
| import math |
|
|
|
|
| def visualize_sensor_layout_ascii(vehicle_config) -> str: |
| """Generate ASCII art of sensor placement on vehicle.""" |
| sc = vehicle_config.sensor_config |
| half_l = vehicle_config.length / 2 |
| half_w = vehicle_config.width / 2 |
| |
| |
| grid_h, grid_w = 30, 50 |
| grid = [[' '] * grid_w for _ in range(grid_h)] |
| |
| |
| scale_x = (grid_w - 10) / (vehicle_config.length + 2) |
| scale_y = (grid_h - 6) / (vehicle_config.width + 2) |
| |
| cx, cy = grid_w // 2, grid_h // 2 |
| |
| |
| vw = int(vehicle_config.length * scale_x / 2) |
| vh = int(vehicle_config.width * scale_y / 2) |
| |
| for x in range(cx - vw, cx + vw + 1): |
| if 0 <= x < grid_w: |
| if 0 <= cy - vh < grid_h: |
| grid[cy - vh][x] = 'β' |
| if 0 <= cy + vh < grid_h: |
| grid[cy + vh][x] = 'β' |
| |
| for y in range(cy - vh, cy + vh + 1): |
| if 0 <= y < grid_h: |
| if 0 <= cx - vw < grid_w: |
| grid[y][cx - vw] = 'β' |
| if 0 <= cx + vw < grid_w: |
| grid[y][cx + vw] = 'β' |
| |
| |
| for (dy, dx), ch in [ |
| ((-vh, -vw), 'β'), ((-vh, vw), 'β'), |
| ((vh, -vw), 'β'), ((vh, vw), 'β') |
| ]: |
| gy, gx = cy + dy, cx + dx |
| if 0 <= gy < grid_h and 0 <= gx < grid_w: |
| grid[gy][gx] = ch |
| |
| |
| if 0 <= cy < grid_h and cx + vw + 1 < grid_w: |
| grid[cy][cx + vw + 1] = 'βΊ' |
| grid[cy][cx] = '+' |
| |
| |
| for i, cam in enumerate(sc.cameras): |
| gx = cx + int(cam.placement.x * scale_x) |
| gy = cy - int(cam.placement.y * scale_y) |
| if 0 <= gy < grid_h and 0 <= gx < grid_w: |
| grid[gy][gx] = 'C' |
| |
| |
| for i, us in enumerate(sc.ultrasonics): |
| gx = cx + int(us.placement.x * scale_x) |
| gy = cy - int(us.placement.y * scale_y) |
| if 0 <= gy < grid_h and 0 <= gx < grid_w: |
| if grid[gy][gx] == ' ' or grid[gy][gx] in 'ββ': |
| grid[gy][gx] = 'U' |
| |
| |
| result = "Vehicle Sensor Layout (Top View)\n" |
| result += "C = Camera, U = Ultrasonic, + = Center, βΊ = Forward\n" |
| result += "=" * grid_w + "\n" |
| for row in grid: |
| result += ''.join(row) + "\n" |
| result += "=" * grid_w + "\n" |
| |
| return result |
|
|
|
|
| def format_model_output(output: Dict[str, torch.Tensor]) -> str: |
| """Format model output as human-readable text.""" |
| lines = [] |
| lines.append("ββββββββββββββββββββββββββββββββββββββββββββ") |
| lines.append("β FSD Model Output Summary β") |
| lines.append("β βββββββββββββββββββββββββββββββββββββββββββ£") |
| |
| |
| if "control/steering_deg" in output: |
| steer = output["control/steering_deg"].mean().item() |
| throttle = output["control/throttle"].mean().item() |
| brake = output["control/brake"].mean().item() |
| lines.append(f"β Steering: {steer:+.2f}Β°") |
| lines.append(f"β Throttle: {throttle:.3f}") |
| lines.append(f"β Brake: {brake:.3f}") |
| |
| |
| if "planning/collision_risk" in output: |
| risk = output["planning/collision_risk"].mean().item() |
| emergency = output["planning/emergency_brake"].mean().item() |
| lines.append(f"β Collision Risk: {risk:.3f}") |
| lines.append(f"β Emergency Brake: {emergency:.3f}") |
| |
| |
| if "planning/behavior_logits" in output: |
| behaviors = [ |
| "keep_lane", "turn_left", "turn_right", "change_left", |
| "change_right", "stop", "yield", "park", "reverse", "emergency" |
| ] |
| probs = torch.softmax(output["planning/behavior_logits"], dim=-1) |
| top_prob, top_idx = probs.mean(0).topk(3) |
| lines.append("β Top behaviors:") |
| for p, i in zip(top_prob, top_idx): |
| lines.append(f"β {behaviors[i.item()]}: {p.item():.3f}") |
| |
| |
| if "planning/safe_waypoints" in output: |
| wp = output["planning/safe_waypoints"] |
| lines.append(f"β Planned waypoints: {wp.shape[1]}") |
| lines.append(f"β First: ({wp[0,0,0]:.2f}, {wp[0,0,1]:.2f})") |
| lines.append(f"β Last: ({wp[0,-1,0]:.2f}, {wp[0,-1,1]:.2f})") |
| |
| |
| if "control/controller_weights" in output: |
| w = output["control/controller_weights"].mean(0) |
| lines.append(f"β Controller mix: Neural={w[0]:.2f} Stanley={w[1]:.2f} PID={w[2]:.2f}") |
| |
| lines.append("ββββββββββββββββββββββββββββββββββββββββββββ") |
| |
| return "\n".join(lines) |
|
|
|
|
| def format_parameter_count(counts: Dict[str, int]) -> str: |
| """Format parameter counts as a nice table.""" |
| lines = [] |
| lines.append("βββββββββββββββββββ¬βββββββββββββββ¬ββββββββββββ") |
| lines.append("β Module β Parameters β Size (MB) β") |
| lines.append("βββββββββββββββββββΌβββββββββββββββΌββββββββββββ€") |
| |
| for name, count in counts.items(): |
| if name in ["total", "total_trainable"]: |
| continue |
| size_mb = count * 4 / (1024 * 1024) |
| lines.append(f"β {name:<15} β {count:>12,} β {size_mb:>8.2f} β") |
| |
| lines.append("βββββββββββββββββββΌβββββββββββββββΌββββββββββββ€") |
| total = counts.get("total", 0) |
| trainable = counts.get("total_trainable", 0) |
| total_mb = total * 4 / (1024 * 1024) |
| lines.append(f"β {'TOTAL':<15} β {total:>12,} β {total_mb:>8.2f} β") |
| lines.append(f"β {'Trainable':<15} β {trainable:>12,} β β") |
| lines.append("βββββββββββββββββββ΄βββββββββββββββ΄ββββββββββββ") |
| |
| return "\n".join(lines) |
|
|