Text Metrics for Testing in the Training
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import math
|
import math
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pybullet as p
|
import pybullet as p
|
||||||
import pybullet_data
|
|
||||||
import gymnasium as gym
|
import gymnasium as gym
|
||||||
from gymnasium import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
import config as cfg
|
import config as cfg
|
||||||
import robot_init as ri
|
import robot_init as ri
|
||||||
|
from .sim_manager import SimManager
|
||||||
|
|
||||||
|
|
||||||
class JackBotEnv(gym.Env):
|
class JackBotEnv(gym.Env):
|
||||||
"""Gymnasium environment for joint-command learning with the JackBot URDF."""
|
"""Gymnasium environment for joint-command learning using SimManager for GUI and simulation control."""
|
||||||
|
|
||||||
metadata = {"render_modes": ["human", "rgb_array"]}
|
metadata = {"render_modes": ["human", "rgb_array"]}
|
||||||
|
|
||||||
@@ -26,20 +26,6 @@ class JackBotEnv(gym.Env):
|
|||||||
robot_spacing: float = 0.5,
|
robot_spacing: float = 0.5,
|
||||||
start_pose: str = "init_deg",
|
start_pose: str = "init_deg",
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Initialize the JackBot environment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
urdf_path: Path to the robot's URDF file. Defaults to config value.
|
|
||||||
use_gui: If True, uses p.GUI (shows window), else p.DIRECT (headless).
|
|
||||||
frame_skip: Number of physics simulation steps per agent step.
|
|
||||||
Higher values mean the agent takes actions less frequently (e.g., 4 steps at 240Hz = 60Hz control).
|
|
||||||
random_command: If True, assigns a random velocity command (vx, vy, omega) to each robot every reset.
|
|
||||||
max_episode_steps: Maximum number of steps (interactions) before the episode is truncated.
|
|
||||||
num_robots: Number of robots to spawn for parallel training in the same scene.
|
|
||||||
robot_spacing: Distance in meters between robots when spawning multiple.
|
|
||||||
start_pose: The initial joint configuration name (e.g., 'init_deg' or 'init90_deg').
|
|
||||||
"""
|
|
||||||
self.urdf_path = urdf_path or cfg.urdf_path
|
self.urdf_path = urdf_path or cfg.urdf_path
|
||||||
self.use_gui = use_gui
|
self.use_gui = use_gui
|
||||||
self.frame_skip = frame_skip
|
self.frame_skip = frame_skip
|
||||||
@@ -49,7 +35,10 @@ class JackBotEnv(gym.Env):
|
|||||||
self.robot_spacing = robot_spacing
|
self.robot_spacing = robot_spacing
|
||||||
self.start_pose = start_pose
|
self.start_pose = start_pose
|
||||||
self.action_scale = math.radians(8.0)
|
self.action_scale = math.radians(8.0)
|
||||||
self.physics_client = None
|
|
||||||
|
# Delegate physics simulation & GUI management
|
||||||
|
self.sim_manager = SimManager(use_gui=self.use_gui)
|
||||||
|
|
||||||
self.robots = []
|
self.robots = []
|
||||||
self.plane = None
|
self.plane = None
|
||||||
self.robot_joint_indices = []
|
self.robot_joint_indices = []
|
||||||
@@ -58,6 +47,8 @@ class JackBotEnv(gym.Env):
|
|||||||
self.initial_angles = None
|
self.initial_angles = None
|
||||||
self.commands = None
|
self.commands = None
|
||||||
self.step_count = 0
|
self.step_count = 0
|
||||||
|
self.episode_count = 0
|
||||||
|
self.cumulative_reward = 0.0
|
||||||
self.last_action = None
|
self.last_action = None
|
||||||
self._first_reset = True
|
self._first_reset = True
|
||||||
self._min_steps_before_done = 150
|
self._min_steps_before_done = 150
|
||||||
@@ -69,55 +60,37 @@ class JackBotEnv(gym.Env):
|
|||||||
action_dim = total_joints
|
action_dim = total_joints
|
||||||
|
|
||||||
self.observation_space = spaces.Box(
|
self.observation_space = spaces.Box(
|
||||||
low=-np.inf,
|
low=-np.inf, high=np.inf, shape=(observation_dim,), dtype=np.float32
|
||||||
high=np.inf,
|
|
||||||
shape=(observation_dim,),
|
|
||||||
dtype=np.float32,
|
|
||||||
)
|
)
|
||||||
self.action_space = spaces.Box(
|
self.action_space = spaces.Box(
|
||||||
low=-1.0,
|
low=-1.0, high=1.0, shape=(action_dim,), dtype=np.float32
|
||||||
high=1.0,
|
|
||||||
shape=(action_dim,),
|
|
||||||
dtype=np.float32,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _robot_base_position(self, robot_id: int, num_robots: int = 1, spacing: float = 0.5) -> list[float]:
|
||||||
|
"""Calculates grid coordinates for spawning multiple robots in PyBullet."""
|
||||||
|
cols = int(math.sqrt(num_robots - 1)) + 1
|
||||||
|
row = robot_id // cols
|
||||||
|
col = robot_id % cols
|
||||||
|
x = (col - (cols - 1) / 2.0) * spacing
|
||||||
|
y = (row - (cols - 1) / 2.0) * spacing
|
||||||
|
return [x, y, 0.1]
|
||||||
|
|
||||||
def _connect_sim(self):
|
def _connect_sim(self):
|
||||||
if self.physics_client is not None:
|
self.sim_manager.connect()
|
||||||
p.disconnect(self.physics_client)
|
self.plane, self.robots, self.robot_joint_indices = self.sim_manager.load_scene(
|
||||||
|
self.urdf_path, self.num_robots, self.robot_spacing, self._robot_base_position
|
||||||
flags = p.GUI if self.use_gui else p.DIRECT
|
)
|
||||||
self.physics_client = p.connect(flags)
|
|
||||||
p.setAdditionalSearchPath(pybullet_data.getDataPath())
|
|
||||||
p.setGravity(0, 0, -9.81)
|
|
||||||
|
|
||||||
self.plane = p.loadURDF("plane.urdf")
|
|
||||||
self.robots = []
|
|
||||||
self.robot_joint_indices = []
|
|
||||||
|
|
||||||
for robot_id in range(self.num_robots):
|
|
||||||
base_pos = self._robot_base_position(robot_id)
|
|
||||||
robot = p.loadURDF(self.urdf_path, basePosition=base_pos, useFixedBase=False)
|
|
||||||
self.robots.append(robot)
|
|
||||||
joint_indices = [
|
|
||||||
i
|
|
||||||
for i in range(p.getNumJoints(robot))
|
|
||||||
if p.getJointInfo(robot, i)[2] == p.JOINT_REVOLUTE
|
|
||||||
]
|
|
||||||
self.robot_joint_indices.append(joint_indices)
|
|
||||||
|
|
||||||
joint_count = len(self.robot_joint_indices[0])
|
joint_count = len(self.robot_joint_indices[0])
|
||||||
if any(len(indices) != joint_count for indices in self.robot_joint_indices):
|
if any(len(indices) != joint_count for indices in self.robot_joint_indices):
|
||||||
raise ValueError("All robots must have the same number of revolute joints.")
|
raise ValueError("All robots must have the same number of revolute joints.")
|
||||||
|
|
||||||
lower_limits = []
|
lower_limits, upper_limits = [], []
|
||||||
upper_limits = []
|
|
||||||
for joint_index in self.robot_joint_indices[0]:
|
for joint_index in self.robot_joint_indices[0]:
|
||||||
info = p.getJointInfo(self.robots[0], joint_index)
|
info = p.getJointInfo(self.robots[0], joint_index)
|
||||||
lower = info[8]
|
lower, upper = info[8], info[9]
|
||||||
upper = info[9]
|
|
||||||
if lower >= upper:
|
if lower >= upper:
|
||||||
lower = -math.pi
|
lower, upper = -math.pi, math.pi
|
||||||
upper = math.pi
|
|
||||||
lower_limits.append(lower)
|
lower_limits.append(lower)
|
||||||
upper_limits.append(upper)
|
upper_limits.append(upper)
|
||||||
|
|
||||||
@@ -128,53 +101,27 @@ class JackBotEnv(gym.Env):
|
|||||||
pose = ri.init_deg if self.start_pose == "init_deg" else ri.init90_deg
|
pose = ri.init_deg if self.start_pose == "init_deg" else ri.init90_deg
|
||||||
initial_pose = pose.to_rad().data.flatten()
|
initial_pose = pose.to_rad().data.flatten()
|
||||||
self.initial_angles = np.tile(initial_pose, (self.num_robots, 1)).astype(np.float32)
|
self.initial_angles = np.tile(initial_pose, (self.num_robots, 1)).astype(np.float32)
|
||||||
self.initial_angles = np.clip(
|
self.initial_angles = np.clip(self.initial_angles, self.joint_lower, self.joint_upper)
|
||||||
self.initial_angles,
|
|
||||||
self.joint_lower,
|
|
||||||
self.joint_upper,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.last_action = np.zeros(self.num_robots * joint_count, dtype=np.float32)
|
self.last_action = np.zeros(self.num_robots * joint_count, dtype=np.float32)
|
||||||
self.commands = np.zeros((self.num_robots, 4), dtype=np.float32)
|
self.commands = np.zeros((self.num_robots, 4), dtype=np.float32)
|
||||||
|
|
||||||
def reset(self, command: np.ndarray | None = None, seed: int | None = None, options: dict | None = None):
|
def reset(self, command: np.ndarray | None = None, seed: int | None = None, options: dict | None = None):
|
||||||
if self._first_reset:
|
self.episode_count += 1
|
||||||
self.step_count = 0
|
self.step_count = 0
|
||||||
self.last_action = np.zeros(self.num_robots * len(self.robot_joint_indices[0]), dtype=np.float32)
|
self.cumulative_reward = 0.0
|
||||||
|
|
||||||
if seed is not None:
|
|
||||||
self.seed(seed)
|
|
||||||
|
|
||||||
if command is not None:
|
|
||||||
self.commands = np.array(command, dtype=np.float32).reshape(self.num_robots, 4)
|
|
||||||
elif self.random_command:
|
|
||||||
self.commands = np.stack([self.sample_command() for _ in range(self.num_robots)])
|
|
||||||
else:
|
|
||||||
self.commands = np.zeros((self.num_robots, 4), dtype=np.float32)
|
|
||||||
|
|
||||||
self._reset_pose()
|
|
||||||
self._first_reset = False
|
|
||||||
return self._get_obs(), {}
|
|
||||||
|
|
||||||
|
if not self._first_reset:
|
||||||
|
# Reset simulation bodies without reconnecting PyBullet
|
||||||
p.resetSimulation()
|
p.resetSimulation()
|
||||||
p.setGravity(0, 0, -9.81)
|
p.setGravity(0, 0, -9.81)
|
||||||
p.setAdditionalSearchPath(pybullet_data.getDataPath())
|
self.sim_manager.reset_hud()
|
||||||
self.plane = p.loadURDF("plane.urdf")
|
self.plane, self.robots, self.robot_joint_indices = self.sim_manager.load_scene(
|
||||||
self.robots = []
|
self.urdf_path, self.num_robots, self.robot_spacing, self._robot_base_position
|
||||||
self.robot_joint_indices = []
|
)
|
||||||
|
else:
|
||||||
|
self._first_reset = False
|
||||||
|
|
||||||
for robot_id in range(self.num_robots):
|
|
||||||
base_pos = self._robot_base_position(robot_id)
|
|
||||||
robot = p.loadURDF(self.urdf_path, basePosition=base_pos, useFixedBase=False)
|
|
||||||
self.robots.append(robot)
|
|
||||||
joint_indices = [
|
|
||||||
i
|
|
||||||
for i in range(p.getNumJoints(robot))
|
|
||||||
if p.getJointInfo(robot, i)[2] == p.JOINT_REVOLUTE
|
|
||||||
]
|
|
||||||
self.robot_joint_indices.append(joint_indices)
|
|
||||||
|
|
||||||
self.step_count = 0
|
|
||||||
self.last_action = np.zeros(self.num_robots * len(self.robot_joint_indices[0]), dtype=np.float32)
|
self.last_action = np.zeros(self.num_robots * len(self.robot_joint_indices[0]), dtype=np.float32)
|
||||||
|
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
@@ -188,10 +135,11 @@ class JackBotEnv(gym.Env):
|
|||||||
self.commands = np.zeros((self.num_robots, 4), dtype=np.float32)
|
self.commands = np.zeros((self.num_robots, 4), dtype=np.float32)
|
||||||
|
|
||||||
self._reset_pose()
|
self._reset_pose()
|
||||||
|
self._update_gui_hud(reward=0.0)
|
||||||
|
|
||||||
return self._get_obs(), {}
|
return self._get_obs(), {}
|
||||||
|
|
||||||
def seed(self, seed: int | None = None):
|
def seed(self, seed: int | None = None):
|
||||||
"""Set RNG seed for reproducibility (Gym compatibility)."""
|
|
||||||
if seed is None:
|
if seed is None:
|
||||||
seed = int(np.random.randint(0, 2**31 - 1))
|
seed = int(np.random.randint(0, 2**31 - 1))
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
@@ -252,21 +200,35 @@ class JackBotEnv(gym.Env):
|
|||||||
self.step_count += 1
|
self.step_count += 1
|
||||||
observation = self._get_obs()
|
observation = self._get_obs()
|
||||||
reward = self._compute_reward()
|
reward = self._compute_reward()
|
||||||
|
self.cumulative_reward += reward
|
||||||
|
|
||||||
done = self._is_done()
|
done = self._is_done()
|
||||||
if self.step_count <= self._min_steps_before_done:
|
if self.step_count <= self._min_steps_before_done:
|
||||||
done = False
|
done = False
|
||||||
info = {"step": self.step_count}
|
|
||||||
|
|
||||||
# Gymnasium-style return: (obs, reward, terminated, truncated, info)
|
self._update_gui_hud(reward=reward)
|
||||||
if done:
|
|
||||||
terminated = True
|
terminated = done
|
||||||
truncated = False
|
|
||||||
else:
|
|
||||||
terminated = False
|
|
||||||
truncated = False
|
truncated = False
|
||||||
|
info = {"step": self.step_count, "episode_reward": self.cumulative_reward}
|
||||||
|
|
||||||
return observation, float(reward), terminated, truncated, info
|
return observation, float(reward), terminated, truncated, info
|
||||||
|
|
||||||
|
def _update_gui_hud(self, reward: float):
|
||||||
|
"""Passes current state metrics to the SimManager HUD renderer."""
|
||||||
|
linear_vel, _ = p.getBaseVelocity(self.robots[0])
|
||||||
|
cmd = self.commands[0]
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"Episode": self.episode_count,
|
||||||
|
"Step": f"{self.step_count} / {self.max_episode_steps}",
|
||||||
|
"Step Reward": reward,
|
||||||
|
"Total Reward": self.cumulative_reward,
|
||||||
|
"Target Cmd (vx,vy,w)": [cmd[0], cmd[1], cmd[3]],
|
||||||
|
"Actual Vel (vx,vy)": [linear_vel[0], linear_vel[1]],
|
||||||
|
}
|
||||||
|
self.sim_manager.update_hud(stats)
|
||||||
|
|
||||||
def _compute_reward(self) -> float:
|
def _compute_reward(self) -> float:
|
||||||
rewards = []
|
rewards = []
|
||||||
for robot_id, robot in enumerate(self.robots):
|
for robot_id, robot in enumerate(self.robots):
|
||||||
@@ -294,24 +256,7 @@ class JackBotEnv(gym.Env):
|
|||||||
if position[2] < 0.02:
|
if position[2] < 0.02:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.step_count >= self.max_episode_steps:
|
return self.step_count >= self.max_episode_steps
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def render(self, mode="human"):
|
|
||||||
if self.use_gui:
|
|
||||||
return None
|
|
||||||
raise NotImplementedError("Render is only supported with use_gui=True")
|
|
||||||
|
|
||||||
def _robot_base_position(self, robot_id: int) -> list[float]:
|
|
||||||
cols = int(math.sqrt(self.num_robots - 1)) + 1
|
|
||||||
row = robot_id // cols
|
|
||||||
col = robot_id % cols
|
|
||||||
x = (col - (cols - 1) / 2.0) * self.robot_spacing
|
|
||||||
y = (row - (cols - 1) / 2.0) * self.robot_spacing
|
|
||||||
return [x, y, 0.1]
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.physics_client is not None:
|
self.sim_manager.disconnect()
|
||||||
p.disconnect(self.physics_client)
|
|
||||||
self.physics_client = None
|
|
||||||
+1
-1
@@ -30,7 +30,7 @@ class SimManager:
|
|||||||
p.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)
|
p.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)
|
||||||
p.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, 0)
|
p.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, 0)
|
||||||
p.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, 0)
|
p.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, 0)
|
||||||
p.configureDebugVisualizer(p.COV_ENABLE_RGB_OUTPUT_PREVIEW, 0)
|
p.configureDebugVisualizer(p.COV_ENABLE_RGB_BUFFER_PREVIEW, 0)
|
||||||
|
|
||||||
def load_scene(
|
def load_scene(
|
||||||
self, urdf_path: str, num_robots: int, robot_spacing: float, base_pos_fn
|
self, urdf_path: str, num_robots: int, robot_spacing: float, base_pos_fn
|
||||||
|
|||||||
Reference in New Issue
Block a user