Files
JackBot/ml/MetricsOverlay.py
JackM323 14251aa415 Readme and comments and reward pdated
readme was outdated
env rewards got a penalty for standing still while it should move instead of 0 reward
2026-09-13 13:28:24 +02:00

121 lines
4.5 KiB
Python

"""
ml/MetricsOverlay.py - 3D HUD overlay for live simulation telemetry.
This file provides a small PyBullet HUD renderer that draws live robot metrics in the
simulation scene. It is used during GUI runs to show the current phase, command vector,
reward information, and basic motion statistics without leaving the 3D view.
"""
from typing import List, Tuple, Optional, Dict
import numpy as np
import pybullet as p
class MetricsHUD:
"""Renders real-time telemetry as black text floating in 3D, always facing the active camera."""
def __init__(self, physics_client_id: int = 0):
self.client_id = physics_client_id
self._text_id: Optional[int] = None
def _get_camera_facing_orientation(self) -> List[float]:
"""Calculates a quaternion that points the text towards the current GUI camera."""
try:
cam_info = p.getDebugVisualizerCamera(physicsClientId=self.client_id)
# cam_info index 8: yaw, index 9: pitch
yaw = cam_info[8]
pitch = cam_info[9]
pitch_rad = np.radians(pitch + 90.0)
yaw_rad = np.radians(yaw)
text_orientation = p.getQuaternionFromEuler(
[pitch_rad, 0.0, yaw_rad],
physicsClientId=self.client_id
)
return text_orientation
except Exception:
return [0.0, 0.0, 0.0, 1.0]
def update(
self,
episode: int,
step: int,
robot_rewards: List[float],
cmd_vel: np.ndarray,
fps: float = 0.0,
height: float = 0.0,
roll_pitch: Tuple[float, float] = (0.0, 0.0),
mode: str = "direct",
phase: str = "STAND_ONLY",
distance: float = 0.0,
status: str = "ALIVE",
reward_components: Optional[Dict[str, float]] = None,
ep_step: int = 0,
) -> None:
"""Updates floating text block in 3D space with expanded telemetry."""
sorted_rewards = sorted(robot_rewards, reverse=True)
top1 = f"{sorted_rewards[0]:+.2f}" if len(sorted_rewards) > 0 else "0.00"
vx = cmd_vel[0] if len(cmd_vel) > 0 else 0.0
vy = cmd_vel[1] if len(cmd_vel) > 1 else 0.0
omega = cmd_vel[2] if len(cmd_vel) > 2 else 0.0
lines = [
"=== JACKBOT TELEMETRY ===",
f"Mode: {mode.upper()}",
f"Curriculum: {phase}",
f"Status: {status}",
f"Episode: {episode} (Step {ep_step})",
f"Global Step: {step}",
f"FPS: {fps:.1f}",
"-------------------------",
f"Episode Rew: {top1}",
f"Cmd (X,Y,W): [{vx:+.2f}, {vy:+.2f}, {omega:+.2f}]",
f"Height: {height:.3f} m",
f"Roll/Pitch: {roll_pitch[0]:+.1f}° / {roll_pitch[1]:+.1f}°",
f"Max Dist: {distance:.2f} m",
]
if reward_components:
lin_v = reward_components.get("lin_vel", 0.0)
stab = reward_components.get("stability", 0.0)
h_rew = reward_components.get("height", 0.0)
jit = reward_components.get("jitter_penalty", 0.0)
lines.append("--- Reward Components ---")
lines.append(f"LinVel: {lin_v:.2f} | Stab: {stab:.2f}")
lines.append(f"Height: {h_rew:.2f} | Jitter: {jit:+.3f}")
hud_text = "\n".join(lines)
# Position above origin in simulation world
text_position = [-0.8, -0.8, 1.2]
text_color = [0, 0, 0] # Pure black
# Calculate dynamic orientation to align text flat against camera plane
text_orientation = self._get_camera_facing_orientation()
# Safely remove old text to prevent PyBullet ghosting/overlapping
if self._text_id is not None:
try:
p.removeUserDebugItem(self._text_id, physicsClientId=self.client_id)
except Exception:
pass
# Draw fresh text
self._text_id = p.addUserDebugText(
text=hud_text,
textPosition=text_position,
textColorRGB=text_color,
textSize=0.085,
textOrientation=text_orientation,
physicsClientId=self.client_id
)
def reset(self) -> None:
"""Removes the active debug text item so a new episode starts with a clean overlay."""
if self._text_id is not None:
try:
p.removeUserDebugItem(self._text_id, physicsClientId=self.client_id)
except Exception:
pass
self._text_id = None